evolclaw 3.1.3 → 3.1.5

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.
Files changed (100) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/assets/.env.template +4 -0
  3. package/assets/config.json.template +6 -0
  4. package/assets/wechat-group-qr.jpeg +0 -0
  5. package/dist/agents/claude-runner.js +348 -156
  6. package/dist/agents/kit-renderer.js +211 -42
  7. package/dist/aun/aid/agentmd.js +75 -139
  8. package/dist/aun/aid/client.js +1 -14
  9. package/dist/aun/aid/identity.js +381 -54
  10. package/dist/aun/aid/index.js +3 -2
  11. package/dist/aun/aid/store.js +74 -0
  12. package/dist/aun/msg/p2p.js +26 -2
  13. package/dist/aun/rpc/connection.js +23 -35
  14. package/dist/channels/aun.js +92 -144
  15. package/dist/channels/dingtalk.js +1 -0
  16. package/dist/channels/feishu.js +270 -190
  17. package/dist/channels/qqbot.js +1 -0
  18. package/dist/channels/wechat.js +1 -0
  19. package/dist/channels/wecom.js +1 -0
  20. package/dist/cli/agent.js +26 -27
  21. package/dist/cli/bench.js +45 -34
  22. package/dist/cli/help.js +23 -0
  23. package/dist/cli/index.js +538 -77
  24. package/dist/cli/init-channel.js +7 -4
  25. package/dist/cli/link-rules.js +2 -1
  26. package/dist/cli/model.js +324 -0
  27. package/dist/cli/net-check.js +138 -56
  28. package/dist/cli/watch-msg.js +7 -7
  29. package/dist/cli/watch-web/debug-log.js +18 -0
  30. package/dist/cli/watch-web/server.js +306 -0
  31. package/dist/cli/watch-web/sources/aid.js +63 -0
  32. package/dist/cli/watch-web/sources/msg.js +70 -0
  33. package/dist/cli/watch-web/sources/session.js +638 -0
  34. package/dist/cli/watch-web/sources/types.js +10 -0
  35. package/dist/cli/watch-web/static/app.js +546 -0
  36. package/dist/cli/watch-web/static/index.html +54 -0
  37. package/dist/cli/watch-web/static/style.css +247 -0
  38. package/dist/core/channel-loader.js +7 -4
  39. package/dist/core/command-handler.js +87 -93
  40. package/dist/core/evolagent-registry.js +1 -1
  41. package/dist/core/evolagent.js +4 -4
  42. package/dist/core/interaction-router.js +59 -0
  43. package/dist/core/message/message-bridge.js +6 -6
  44. package/dist/core/message/message-log.js +2 -2
  45. package/dist/core/message/message-processor.js +104 -118
  46. package/dist/core/message/stream-idle-monitor.js +21 -0
  47. package/dist/core/model/model-catalog.js +215 -0
  48. package/dist/core/model/model-scope.js +250 -0
  49. package/dist/core/relation/peer-identity.js +78 -44
  50. package/dist/core/relation/peer-key.js +16 -0
  51. package/dist/core/session/session-fs-store.js +34 -55
  52. package/dist/core/session/session-key.js +24 -0
  53. package/dist/core/session/session-manager.js +312 -251
  54. package/dist/core/session/session-mapper.js +9 -4
  55. package/dist/core/trigger/manager.js +37 -0
  56. package/dist/core/trigger/scheduler.js +2 -1
  57. package/dist/index.js +10 -3
  58. package/dist/ipc.js +22 -0
  59. package/dist/paths.js +87 -16
  60. package/dist/utils/npm-ops.js +18 -11
  61. package/kits/docs/GUIDE.md +2 -2
  62. package/kits/docs/INDEX.md +11 -7
  63. package/kits/docs/channels/aun.md +56 -17
  64. package/kits/docs/channels/feishu.md +41 -12
  65. package/kits/docs/context-assembly.md +181 -0
  66. package/kits/docs/evolclaw/agent.md +49 -0
  67. package/kits/docs/evolclaw/aid.md +49 -0
  68. package/kits/docs/evolclaw/ctl.md +46 -0
  69. package/kits/docs/evolclaw/group.md +82 -0
  70. package/kits/docs/evolclaw/msg.md +86 -0
  71. package/kits/docs/evolclaw/rpc.md +35 -0
  72. package/kits/docs/evolclaw/storage.md +49 -0
  73. package/kits/docs/venues/aun-group.md +10 -0
  74. package/kits/docs/venues/aun-private.md +10 -0
  75. package/kits/docs/venues/client-desktop.md +10 -0
  76. package/kits/docs/venues/client-mobile.md +10 -0
  77. package/kits/docs/venues/feishu-group.md +13 -0
  78. package/kits/docs/venues/feishu-private.md +9 -0
  79. package/kits/docs/venues/group.md +11 -0
  80. package/kits/docs/venues/private.md +10 -0
  81. package/kits/eck_manifest.json +75 -39
  82. package/kits/rules/01-overview.md +20 -10
  83. package/kits/rules/05-venue.md +2 -2
  84. package/kits/rules/06-channel.md +30 -27
  85. package/kits/templates/system-fragments/baseagent.md +7 -1
  86. package/kits/templates/system-fragments/channel.md +4 -1
  87. package/kits/templates/system-fragments/identity.md +4 -4
  88. package/kits/templates/system-fragments/relation.md +8 -5
  89. package/kits/templates/system-fragments/session.md +27 -0
  90. package/kits/templates/system-fragments/venue.md +13 -1
  91. package/package.json +13 -6
  92. package/dist/aun/aid/lifecycle-log.js +0 -33
  93. package/dist/net-check.js +0 -640
  94. package/dist/utils/aid-lifecycle-log.js +0 -33
  95. package/dist/watch-msg.js +0 -544
  96. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  97. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  98. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  99. package/kits/docs/evolclaw/tools.md +0 -25
  100. package/kits/templates/system-fragments/eckruntime.md +0 -14
@@ -1,119 +1,446 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  import crypto from 'crypto';
5
- import { getAunClient, downloadCaRoot } from './client.js';
6
- import { resolvePaths, aidsDir as evolclawAidsDir, agentMdPath } from '../../paths.js';
4
+ import { getAidStore, loadAid, loadClient, AidLoadError, SLOT } from './store.js';
5
+ import { downloadCaRoot } from './client.js';
6
+ import { resolvePaths, agentMdPath, aunPath as defaultAunPath } from '../../paths.js';
7
7
  // ==================== Validation ====================
8
8
  export function isValidAid(name) {
9
9
  const labels = name.split('.');
10
10
  return labels.length >= 3 && labels.every(l => /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(l));
11
11
  }
12
+ /**
13
+ * 根据扫描得到的状态推断 AID 分类。
14
+ * - hasPrivateKey + signVerified=true → mine
15
+ * - hasPrivateKey + (signVerified=false 或未实测) → broken
16
+ * - !hasPrivateKey + hasCert → peer-cert
17
+ * - !hasPrivateKey + !hasCert → no-cert
18
+ */
19
+ function categorizeAid(info) {
20
+ if (info.hasPrivateKey) {
21
+ if (info.signVerified === true)
22
+ return 'mine';
23
+ return 'broken';
24
+ }
25
+ return info.hasCert ? 'peer-cert' : 'no-cert';
26
+ }
12
27
  // ==================== AID Operations ====================
13
28
  export function aidList(aunPath) {
14
- const aunAidsDir = path.join(aunPath ?? path.join(os.homedir(), '.aun'), 'AIDs');
15
- const ecAidsDir = evolclawAidsDir();
29
+ const root = aunPath ?? defaultAunPath();
30
+ const aunAidsDir = path.join(root, 'AIDs');
16
31
  const seen = new Map();
17
- // Scan ~/.aun/AIDs (private keys live here)
18
32
  if (fs.existsSync(aunAidsDir)) {
19
33
  for (const e of fs.readdirSync(aunAidsDir, { withFileTypes: true })) {
20
34
  if (!e.isDirectory())
21
35
  continue;
36
+ const aidDir = path.join(aunAidsDir, e.name);
37
+ const keyJsonPath = path.join(aidDir, 'private', 'key.json');
38
+ const hasPrivateKey = fs.existsSync(path.join(aidDir, 'private'));
39
+ const certPath = path.join(aidDir, 'public', 'cert.pem');
40
+ let hasCert = false;
41
+ let certExpired = false;
42
+ let keyMatchesCert = null;
43
+ if (fs.existsSync(certPath)) {
44
+ hasCert = true;
45
+ try {
46
+ const certPem = fs.readFileSync(certPath, 'utf-8');
47
+ const x509 = new crypto.X509Certificate(certPem);
48
+ certExpired = new Date(x509.validTo) < new Date();
49
+ if (hasPrivateKey && fs.existsSync(keyJsonPath)) {
50
+ try {
51
+ const kp = JSON.parse(fs.readFileSync(keyJsonPath, 'utf-8'));
52
+ const localPubB64 = typeof kp?.public_key_der_b64 === 'string' ? kp.public_key_der_b64 : '';
53
+ if (localPubB64) {
54
+ const certPubDer = x509.publicKey.export({ type: 'spki', format: 'der' });
55
+ const localPubDer = Buffer.from(localPubB64, 'base64');
56
+ keyMatchesCert = certPubDer.equals(localPubDer);
57
+ }
58
+ }
59
+ catch { /* key.json 不可解析视作不匹配 */
60
+ keyMatchesCert = false;
61
+ }
62
+ }
63
+ }
64
+ catch {
65
+ // 证书无法解析视为过期 / 不可用
66
+ certExpired = true;
67
+ }
68
+ }
22
69
  seen.set(e.name, {
23
70
  aid: e.name,
24
- hasPrivateKey: fs.existsSync(path.join(aunAidsDir, e.name, 'private')),
71
+ category: categorizeAid({
72
+ hasPrivateKey,
73
+ hasCert,
74
+ // 静态扫描没跑实测,用 canSign 作为 mine/broken 的近似判定
75
+ signVerified: hasPrivateKey ? (hasCert && !certExpired && keyMatchesCert === true) : null,
76
+ canSign: hasPrivateKey && hasCert && !certExpired && keyMatchesCert === true,
77
+ }),
78
+ hasPrivateKey,
25
79
  hasAgentMd: fs.existsSync(agentMdPath(e.name)),
80
+ hasCert,
81
+ certExpired,
82
+ keyMatchesCert,
83
+ canSign: hasPrivateKey && hasCert && !certExpired && keyMatchesCert === true,
84
+ signVerified: null,
26
85
  });
27
86
  }
28
87
  }
29
- // Scan $EVOLCLAW_HOME/AIDs (agent.md lives here)
30
- if (fs.existsSync(ecAidsDir) && ecAidsDir !== aunAidsDir) {
31
- for (const e of fs.readdirSync(ecAidsDir, { withFileTypes: true })) {
32
- if (!e.isDirectory())
33
- continue;
34
- if (seen.has(e.name))
88
+ return [...seen.values()];
89
+ }
90
+ // ==================== Sign Self-Test ====================
91
+ /**
92
+ * 实跑一次本地 sign + verify,验证 AID 是否真能签名/验签。
93
+ * 全本地(不联网):用 SDK 解密私钥 → ECDSA 签 payload → 用本地 cert 公钥验。
94
+ * 任一环节失败都视为不可签——包括私钥 passphrase 解不开、cert 被 SDK 主动 discard 等。
95
+ */
96
+ export async function verifySignAbility(aid, opts) {
97
+ const aunPath = opts?.aunPath ?? defaultAunPath();
98
+ let ownStore = null;
99
+ try {
100
+ let store = opts?.store;
101
+ if (!store) {
102
+ store = await getAidStore({ slotId: SLOT.cli, aunPath });
103
+ ownStore = store;
104
+ }
105
+ // 加载本地 AID 值对象(含私钥),sign + verify 全本地完成
106
+ let aidObj;
107
+ try {
108
+ aidObj = loadAid(store, aid);
109
+ }
110
+ catch (e) {
111
+ const code = e instanceof AidLoadError ? e.code : 'LOAD_FAILED';
112
+ return { ok: false, reason: `load failed: ${code} ${String(e?.message || e).slice(0, 100)}` };
113
+ }
114
+ const probe = `# probe\naid: "${aid}"\n`;
115
+ const signRes = aidObj.signAgentMd(probe);
116
+ if (!signRes.ok) {
117
+ return { ok: false, reason: `sign failed: ${String(signRes.error?.message || signRes.error?.code).slice(0, 120)}` };
118
+ }
119
+ const verifyRes = aidObj.verifyAgentMd(signRes.data.signed);
120
+ if (!verifyRes.ok) {
121
+ return { ok: false, reason: `verify threw: ${String(verifyRes.error?.message || verifyRes.error?.code).slice(0, 120)}` };
122
+ }
123
+ if (verifyRes.data.status === 'verified')
124
+ return { ok: true };
125
+ return { ok: false, reason: `verify failed: ${verifyRes.data.status ?? 'unknown'}${verifyRes.data.reason ? ' — ' + verifyRes.data.reason : ''}` };
126
+ }
127
+ finally {
128
+ if (ownStore) {
129
+ try {
130
+ ownStore.close();
131
+ }
132
+ catch { /* ignore */ }
133
+ }
134
+ }
135
+ }
136
+ /**
137
+ * aidList 的"实测版":先做静态扫描,再对每个 AID 跑一次本地 sign+verify。
138
+ * 共用同一个 AUNClient 实例,避免重复初始化 secret-store / sqlite。
139
+ */
140
+ export async function aidListVerified(aunPath) {
141
+ const list = aidList(aunPath);
142
+ const root = aunPath ?? defaultAunPath();
143
+ const store = await getAidStore({ slotId: SLOT.cli, aunPath: root });
144
+ try {
145
+ for (const a of list) {
146
+ // canSign=false 的 AID 不必跑实测,结论已经明确
147
+ if (!a.canSign) {
148
+ a.signVerified = false;
149
+ if (!a.hasPrivateKey)
150
+ a.signError = 'no private key';
151
+ else if (!a.hasCert)
152
+ a.signError = 'no cert';
153
+ else if (a.certExpired)
154
+ a.signError = 'cert expired';
155
+ else if (a.keyMatchesCert === false)
156
+ a.signError = 'key/cert public-key mismatch';
157
+ a.category = categorizeAid({
158
+ hasPrivateKey: a.hasPrivateKey, hasCert: a.hasCert,
159
+ signVerified: a.signVerified, canSign: a.canSign,
160
+ });
35
161
  continue;
36
- seen.set(e.name, {
37
- aid: e.name,
38
- hasPrivateKey: fs.existsSync(path.join(aunAidsDir, e.name, 'private')),
39
- hasAgentMd: fs.existsSync(agentMdPath(e.name)),
162
+ }
163
+ const r = await verifySignAbility(a.aid, { aunPath: root, store });
164
+ a.signVerified = r.ok;
165
+ if (!r.ok)
166
+ a.signError = r.reason;
167
+ a.category = categorizeAid({
168
+ hasPrivateKey: a.hasPrivateKey, hasCert: a.hasCert,
169
+ signVerified: a.signVerified, canSign: a.canSign,
40
170
  });
41
171
  }
42
172
  }
43
- return [...seen.values()];
173
+ finally {
174
+ try {
175
+ store.close();
176
+ }
177
+ catch { /* ignore */ }
178
+ }
179
+ return list;
44
180
  }
45
181
  export async function aidCreate(aid, opts) {
46
- const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
182
+ const aunPath = opts?.aunPath ?? defaultAunPath();
47
183
  const aidDir = path.join(aunPath, 'AIDs', aid);
48
- if (fs.existsSync(aidDir) && fs.existsSync(path.join(aidDir, 'private'))) {
49
- const client = await getAunClient(aid, { aunPath });
50
- return { aid, alreadyExisted: true, gateway: '', client };
51
- }
52
- const { AUNClient, GatewayDiscovery } = await import('@agentunion/fastaun');
53
- let client = new AUNClient({ aun_path: aunPath });
54
- try {
55
- const result = await client.auth.createAid({ aid });
56
- const gateway = result.gateway || '';
57
- const caDownloaded = await downloadCaRoot(aunPath, gateway);
58
- const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
59
- if (caDownloaded && fs.existsSync(caCertPath)) {
184
+ const hasPrivateKey = fs.existsSync(path.join(aidDir, 'private'));
185
+ // 如果私钥已存在,先验证签名能力
186
+ if (hasPrivateKey) {
187
+ const verifyResult = await verifySignAbility(aid, { aunPath });
188
+ if (verifyResult.ok) {
189
+ // 身份有效:加载并认证,返回已认证的 client
190
+ const store = await getAidStore({ slotId: SLOT.cli, aunPath });
60
191
  try {
61
- await client.close();
192
+ const client = await loadClient(store, aid);
193
+ const auth = await client.authenticate();
194
+ return { aid, alreadyExisted: true, gateway: String(auth?.gateway ?? ''), client, store };
62
195
  }
63
- catch { /* ignore */ }
64
- client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath });
65
- await client.auth.createAid({ aid });
66
- }
67
- let gatewayUrl = gateway;
68
- if (!gatewayUrl) {
69
- try {
70
- const discovery = new GatewayDiscovery({});
71
- gatewayUrl = await discovery.discover(`https://${aid}/.well-known/aun-gateway`);
196
+ catch (e) {
197
+ store.close();
198
+ throw e;
72
199
  }
73
- catch { /* fall through */ }
74
200
  }
75
- if (gatewayUrl) {
76
- client._gatewayUrl = gatewayUrl;
201
+ // 签名验证失败
202
+ if (!opts?.force) {
203
+ const error = new Error(`AID ${aid} 已存在但身份无效(${verifyResult.reason || '签名验证失败'})。\n` +
204
+ `使用 --force 参数尝试恢复或重新注册。`);
205
+ error.code = 'AID_INVALID';
206
+ error.reason = verifyResult.reason;
207
+ throw error;
208
+ }
209
+ // --force:先尝试 authenticate 恢复证书
210
+ const recoverStore = await getAidStore({ slotId: SLOT.cli, aunPath });
211
+ try {
212
+ const recoverClient = await loadClient(recoverStore, aid);
213
+ const auth = await recoverClient.authenticate();
214
+ return { aid, alreadyExisted: true, gateway: String(auth?.gateway ?? ''), client: recoverClient, store: recoverStore };
215
+ }
216
+ catch {
217
+ recoverStore.close();
218
+ fs.rmSync(aidDir, { recursive: true, force: true });
77
219
  }
78
- return { aid, alreadyExisted: false, gateway: gatewayUrl, client };
79
220
  }
80
- catch (e) {
221
+ // 新注册流程:AIDStore.register → downloadCaRoot → load → authenticate
222
+ const store = await getAidStore({ slotId: SLOT.cli, aunPath });
223
+ try {
224
+ const regResult = await store.register(aid);
225
+ if (!regResult.ok) {
226
+ const e = new Error(regResult.error.message);
227
+ e.code = regResult.error.code;
228
+ throw e;
229
+ }
230
+ // 注册成功后下载 CA 根证书(如果还没有)
231
+ // 从 AID well-known 发现 gateway 用于 CA 下载
232
+ let gatewayUrl = '';
81
233
  try {
82
- await client.close();
234
+ const { GatewayDiscovery } = await import('@agentunion/fastaun');
235
+ const discovery = new GatewayDiscovery({});
236
+ gatewayUrl = await discovery.discover(`https://${aid}/.well-known/aun-gateway`);
83
237
  }
84
- catch { /* ignore */ }
238
+ catch { /* fall through */ }
239
+ if (gatewayUrl) {
240
+ await downloadCaRoot(aunPath, gatewayUrl);
241
+ }
242
+ // 重建 store(CA 可能刚下载,需要 rootCaPath 生效)
243
+ store.close();
244
+ const store2 = await getAidStore({ slotId: SLOT.cli, aunPath });
245
+ try {
246
+ const client = await loadClient(store2, aid);
247
+ await client.authenticate();
248
+ return { aid, alreadyExisted: false, gateway: gatewayUrl, client, store: store2 };
249
+ }
250
+ catch (e) {
251
+ store2.close();
252
+ throw e;
253
+ }
254
+ }
255
+ catch (e) {
256
+ store.close();
85
257
  throw e;
86
258
  }
87
259
  }
88
260
  // ==================== Show ====================
89
- export function aidShow(aid, opts) {
90
- const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
261
+ export async function aidShow(aid, opts) {
262
+ const aunPath = opts?.aunPath ?? defaultAunPath();
91
263
  const aidDir = path.join(aunPath, 'AIDs', aid);
92
264
  const hasPrivateKey = fs.existsSync(path.join(aidDir, 'private'));
93
265
  const hasAgentMd = fs.existsSync(agentMdPath(aid));
94
266
  let certExpiresAt = null;
95
267
  let certSubject = null;
268
+ let certExpired = false;
269
+ let certPem = null;
270
+ let keyMatchesCert = null;
96
271
  const certPath = path.join(aidDir, 'public', 'cert.pem');
97
272
  if (fs.existsSync(certPath)) {
98
273
  try {
99
- const pem = fs.readFileSync(certPath, 'utf-8');
100
- const x509 = new crypto.X509Certificate(pem);
274
+ certPem = fs.readFileSync(certPath, 'utf-8');
275
+ const x509 = new crypto.X509Certificate(certPem);
101
276
  certExpiresAt = x509.validTo;
102
277
  certSubject = x509.subject;
278
+ certExpired = new Date(x509.validTo) < new Date();
279
+ const keyJsonPath = path.join(aidDir, 'private', 'key.json');
280
+ if (hasPrivateKey && fs.existsSync(keyJsonPath)) {
281
+ try {
282
+ const kp = JSON.parse(fs.readFileSync(keyJsonPath, 'utf-8'));
283
+ const localPubB64 = typeof kp?.public_key_der_b64 === 'string' ? kp.public_key_der_b64 : '';
284
+ if (localPubB64) {
285
+ const certPubDer = x509.publicKey.export({ type: 'spki', format: 'der' });
286
+ const localPubDer = Buffer.from(localPubB64, 'base64');
287
+ keyMatchesCert = certPubDer.equals(localPubDer);
288
+ }
289
+ }
290
+ catch {
291
+ keyMatchesCert = false;
292
+ }
293
+ }
103
294
  }
104
295
  catch { /* ignore parse errors */ }
105
296
  }
106
- return { aid, hasPrivateKey, hasAgentMd, certExpiresAt, certSubject };
297
+ let agentMdSignature = 'unknown';
298
+ let agentMdSignatureReason;
299
+ let signVerified = null;
300
+ let signError;
301
+ // 先做一次签名自检(共享 store,避免重复起 SDK)
302
+ const store = await getAidStore({ slotId: SLOT.cli, aunPath });
303
+ try {
304
+ if (hasPrivateKey && certPem && !certExpired && keyMatchesCert !== false) {
305
+ const r = await verifySignAbility(aid, { aunPath, store });
306
+ signVerified = r.ok;
307
+ if (!r.ok)
308
+ signError = r.reason;
309
+ }
310
+ else {
311
+ signVerified = false;
312
+ if (!hasPrivateKey)
313
+ signError = 'no private key';
314
+ else if (!certPem)
315
+ signError = 'no cert';
316
+ else if (certExpired)
317
+ signError = 'cert expired';
318
+ else if (keyMatchesCert === false)
319
+ signError = 'key/cert public-key mismatch';
320
+ }
321
+ if (hasAgentMd) {
322
+ try {
323
+ const content = fs.readFileSync(agentMdPath(aid), 'utf-8');
324
+ if (!content.includes('AUN-SIGNATURE')) {
325
+ agentMdSignature = 'unsigned';
326
+ }
327
+ else {
328
+ // 用本地 AID 值对象验签(含本地 cert 公钥)
329
+ const aidObj = loadAid(store, aid);
330
+ const result = aidObj.verifyAgentMd(content);
331
+ if (!result.ok) {
332
+ agentMdSignature = 'unknown';
333
+ agentMdSignatureReason = String(result.error?.message || result.error?.code).slice(0, 100);
334
+ }
335
+ else if (result.data.status === 'verified') {
336
+ agentMdSignature = 'verified';
337
+ }
338
+ else if (result.data.status === 'unsigned') {
339
+ agentMdSignature = 'unsigned';
340
+ }
341
+ else {
342
+ agentMdSignature = 'invalid';
343
+ agentMdSignatureReason = result.data.reason;
344
+ }
345
+ }
346
+ }
347
+ catch (e) {
348
+ agentMdSignature = 'unknown';
349
+ agentMdSignatureReason = String(e?.message || e).slice(0, 100);
350
+ }
351
+ }
352
+ }
353
+ finally {
354
+ try {
355
+ store.close();
356
+ }
357
+ catch { }
358
+ }
359
+ return { aid, hasPrivateKey, hasAgentMd, certExpiresAt, certSubject, certExpired, keyMatchesCert, signVerified, signError, agentMdSignature, agentMdSignatureReason };
107
360
  }
108
361
  // ==================== Delete ====================
109
362
  export function aidDelete(aid, opts) {
110
- const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
363
+ const aunPath = opts?.aunPath ?? defaultAunPath();
111
364
  const aidDir = path.join(aunPath, 'AIDs', aid);
112
365
  if (!fs.existsSync(aidDir))
113
366
  return false;
114
367
  fs.rmSync(aidDir, { recursive: true, force: true });
115
368
  return true;
116
369
  }
370
+ export async function probePkiRecoverability(aid, opts) {
371
+ const aunPath = opts?.aunPath ?? defaultAunPath();
372
+ const timeoutMs = opts?.timeoutMs ?? 8000;
373
+ const keyJsonPath = path.join(aunPath, 'AIDs', aid, 'private', 'key.json');
374
+ if (!fs.existsSync(keyJsonPath))
375
+ return { kind: 'no-key' };
376
+ let localPubB64 = '';
377
+ try {
378
+ const kp = JSON.parse(fs.readFileSync(keyJsonPath, 'utf-8'));
379
+ if (typeof kp?.public_key_der_b64 !== 'string' || !kp.public_key_der_b64) {
380
+ return { kind: 'unknown', reason: 'key.json missing public_key_der_b64' };
381
+ }
382
+ localPubB64 = kp.public_key_der_b64;
383
+ }
384
+ catch (e) {
385
+ return { kind: 'unknown', reason: `key.json parse failed: ${String(e?.message || e).slice(0, 80)}` };
386
+ }
387
+ // 1. 发现网关
388
+ let gateway = '';
389
+ try {
390
+ const ctl = AbortSignal.timeout(timeoutMs);
391
+ const gwResp = await fetch(`https://${aid}/.well-known/aun-gateway`, { redirect: 'follow', signal: ctl });
392
+ if (gwResp.ok) {
393
+ const text = (await gwResp.text()).trim();
394
+ try {
395
+ const parsed = JSON.parse(text);
396
+ gateway = parsed?.gateways?.[0]?.url || text;
397
+ }
398
+ catch {
399
+ gateway = text;
400
+ }
401
+ }
402
+ }
403
+ catch (e) {
404
+ return { kind: 'no-server-record', reason: `gateway discovery failed: ${String(e?.message || e).slice(0, 80)}` };
405
+ }
406
+ if (!gateway)
407
+ return { kind: 'no-server-record', reason: 'no gateway for AID' };
408
+ // 2. 拉云端 cert
409
+ let certPem = '';
410
+ try {
411
+ const parsed = new URL(gateway);
412
+ const scheme = parsed.protocol === 'wss:' ? 'https:' : 'http:';
413
+ const certUrl = `${scheme}//${parsed.host}/pki/cert/${encodeURIComponent(aid)}`;
414
+ const ctl = AbortSignal.timeout(timeoutMs);
415
+ const resp = await fetch(certUrl, { redirect: 'follow', signal: ctl });
416
+ if (!resp.ok) {
417
+ return { kind: 'no-server-record', reason: `pki/cert HTTP ${resp.status}` };
418
+ }
419
+ certPem = (await resp.text()).trim();
420
+ if (!certPem.includes('BEGIN CERTIFICATE')) {
421
+ return { kind: 'no-server-record', reason: 'pki/cert returned non-cert content' };
422
+ }
423
+ }
424
+ catch (e) {
425
+ return { kind: 'no-server-record', reason: `pki/cert fetch failed: ${String(e?.message || e).slice(0, 80)}` };
426
+ }
427
+ // 3. 比对公钥
428
+ try {
429
+ const x509 = new crypto.X509Certificate(certPem);
430
+ const certPubDer = x509.publicKey.export({ type: 'spki', format: 'der' });
431
+ const localPubDer = Buffer.from(localPubB64, 'base64');
432
+ if (certPubDer.equals(localPubDer)) {
433
+ return { kind: 'recoverable', serverCertPubB64: certPubDer.toString('base64') };
434
+ }
435
+ return {
436
+ kind: 'unrecoverable',
437
+ reason: 'server has different public key registered, current local private key cannot be used',
438
+ };
439
+ }
440
+ catch (e) {
441
+ return { kind: 'unknown', reason: `cert parse failed: ${String(e?.message || e).slice(0, 80)}` };
442
+ }
443
+ }
117
444
  // ==================== Lookup ====================
118
445
  export async function aidLookup(aid) {
119
446
  let gateway = '';
@@ -1,3 +1,4 @@
1
- export { isValidAid, aidList, aidCreate, aidShow, aidDelete, aidLookup, appendAidLifecycle, readAidLifecycle } from './identity.js';
1
+ export { isValidAid, aidList, aidListVerified, aidCreate, aidShow, aidDelete, aidLookup, verifySignAbility, probePkiRecoverability, appendAidLifecycle, readAidLifecycle } from './identity.js';
2
2
  export { buildInitialAgentMd, agentmdGet, agentmdPut, agentmdSync } from './agentmd.js';
3
- export { MIN_AUN_CORE_SDK, AUN_CORE_SDK_PKG, isAunSdkVersionOk, resolveAunCoreSdkPkg, ensureAunSdk, isAunSdkReady, downloadCaRoot, getAunClient, suppressSdkLogs, } from './client.js';
3
+ export { MIN_AUN_CORE_SDK, AUN_CORE_SDK_PKG, isAunSdkVersionOk, resolveAunCoreSdkPkg, ensureAunSdk, isAunSdkReady, downloadCaRoot, suppressSdkLogs, } from './client.js';
4
+ export { getAidStore, loadClient, loadAid, AidLoadError, SLOT } from './store.js';
@@ -0,0 +1,74 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * Slot 命名约定(基于 fastaun 0.4.3 隔离键语义)。
5
+ *
6
+ * slotIsolationKey 取第一个分隔符(空格/'/'/':')之前的部分作为隔离键,
7
+ * 隔离键相同 = 共享消费通道(token / seq 游标 / 消息过滤)。
8
+ *
9
+ * 'evolclaw daemon' → 隔离键 'evolclaw' ┐
10
+ * 'evolclaw cli' → 隔离键 'evolclaw' ├ 共享 evolclaw 消费通道
11
+ * 'evolclaw netcheck' → 隔离键 'evolclaw' ┘
12
+ * 'evolclaw-bench' → 隔离键 'evolclaw-bench'(连字符非分隔符)→ 独立通道
13
+ *
14
+ * 设计意图:
15
+ * - daemon 长连接 + cli 短连接共存于 evolclaw 通道(1 长 + N 短,短连接不踢长连接)
16
+ * - netcheck 长连接抢 evolclaw 长连接位 → 踢掉 daemon(用于踢人/extra_info 测试)
17
+ * - bench 各并发单元用 evolclaw-bench-<N>,各自独立隔离键,互不干扰
18
+ */
19
+ export const SLOT = {
20
+ daemon: 'evolclaw daemon',
21
+ cli: 'evolclaw cli',
22
+ bench: 'evolclaw-bench',
23
+ netcheck: 'evolclaw netcheck',
24
+ };
25
+ /** 加载身份失败时抛出,携带 SDK 的错误码与消息。 */
26
+ export class AidLoadError extends Error {
27
+ code;
28
+ constructor(code, message) {
29
+ super(message);
30
+ this.name = 'AidLoadError';
31
+ this.code = code;
32
+ }
33
+ }
34
+ /**
35
+ * 统一构造 AIDStore。
36
+ *
37
+ * 接管旧 createAunClient 的职责:注入 encryptionSeed、rootCaPath、debug、slotId。
38
+ * deviceId 不传 —— 由 SDK 从 {aunPath}/.device_id 读取或生成(同机共享)。
39
+ */
40
+ export async function getAidStore(opts) {
41
+ const { aunPath: defaultAunPath } = await import('../../paths.js');
42
+ const { loadProcessConfig } = await import('../../config-store.js');
43
+ const { AIDStore } = await import('@agentunion/fastaun');
44
+ const aunPath = opts.aunPath ?? defaultAunPath();
45
+ const encryptionSeed = loadProcessConfig().aun?.encryptionSeed
46
+ ?? process.env.AUN_ENCRYPTION_SEED
47
+ ?? 'evol';
48
+ const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
49
+ const storeOpts = {
50
+ aunPath,
51
+ encryptionSeed,
52
+ slotId: opts.slotId,
53
+ debug: opts.debug ?? false,
54
+ };
55
+ if (fs.existsSync(caCertPath))
56
+ storeOpts.rootCaPath = caCertPath;
57
+ return new AIDStore(storeOpts);
58
+ }
59
+ /**
60
+ * 从 store 加载本地身份并构造已就绪的 AUNClient(尚未连接)。
61
+ *
62
+ * load 失败(证书缺失/过期/链断裂/私钥不匹配等)抛 AidLoadError,携带 SDK 错误码。
63
+ */
64
+ export async function loadClient(store, aid) {
65
+ const { AUNClient } = await import('@agentunion/fastaun');
66
+ return new AUNClient(loadAid(store, aid));
67
+ }
68
+ /** 加载本地 AID 值对象(用于离线签名/验签,无需连接)。load 失败抛 AidLoadError。 */
69
+ export function loadAid(store, aid) {
70
+ const r = store.load(aid);
71
+ if (!r.ok)
72
+ throw new AidLoadError(r.error.code, r.error.message);
73
+ return r.data.aid;
74
+ }
@@ -1,5 +1,6 @@
1
1
  import path from 'path';
2
2
  import { createShortConnection } from '../rpc/index.js';
3
+ import { getAidStore, SLOT } from '../aid/store.js';
3
4
  import { uploadFileAndBuildPayload } from './upload.js';
4
5
  import { appendMessageLog, buildOutboundEntry } from '../../core/message/message-log.js';
5
6
  import { chatDirPath } from '../../core/session/session-fs-store.js';
@@ -7,11 +8,21 @@ import { resolvePaths } from '../../paths.js';
7
8
  export async function msgSend(args) {
8
9
  const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
9
10
  try {
10
- // 1. 解析对端身份(30天缓存)
11
+ // 1. 解析对端身份(30天缓存)。身份解析走 HTTP+PKI(store),与发消息的短连接无关。
11
12
  const { agentsDir } = resolvePaths();
12
13
  const selfAgentDir = path.join(agentsDir, args.from);
13
14
  const { PeerIdentityCache } = await import('../../core/relation/peer-identity.js');
14
- const peerIdentity = await PeerIdentityCache.resolve('aun', args.to, selfAgentDir, conn, false);
15
+ const idStore = await getAidStore({ slotId: args.slotId ?? SLOT.cli, aunPath: args.aunPath });
16
+ let peerIdentity;
17
+ try {
18
+ peerIdentity = await PeerIdentityCache.resolve('aun', args.to, selfAgentDir, idStore, false);
19
+ }
20
+ finally {
21
+ try {
22
+ idStore.close();
23
+ }
24
+ catch { /* ignore */ }
25
+ }
15
26
  // 2. 决定 chatmode(遵循来源1-3)
16
27
  // 私聊:非 human 对端 → proactive,human 对端 → interactive
17
28
  const chatmode = peerIdentity.isAgent ? 'proactive' : 'interactive';
@@ -81,6 +92,19 @@ export async function msgSend(args) {
81
92
  msgType: 'text',
82
93
  source,
83
94
  }));
95
+ // 通知 daemon 更新 stats(如果 daemon 在运行)
96
+ try {
97
+ const { ipcQuery } = await import('../../ipc.js');
98
+ await ipcQuery(resolvePaths().socket, {
99
+ type: 'aun-aid-stats-record-outbound',
100
+ aid: args.from,
101
+ toPeer: args.to,
102
+ text: textContent,
103
+ encrypt: args.encrypt === true,
104
+ chatmode,
105
+ }, 1000);
106
+ }
107
+ catch { /* daemon 不在或 IPC 失败都忽略 */ }
84
108
  }
85
109
  catch { }
86
110
  }