evolclaw 2.6.2 → 2.6.4

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.
@@ -130,6 +130,8 @@ export class AgentRunner {
130
130
  return this.permissionMode;
131
131
  }
132
132
  listModes() {
133
+ // readonly 模式暂时禁用:与 proactive 模式系统提示词存在语义冲突,
134
+ // 且 READONLY_WRITE_PATTERNS 未覆盖 evolclaw ctl send/file,契约不稳固
133
135
  return [
134
136
  { key: 'auto', nameZh: '自动', description: 'AI 分类器自动判断', available: true },
135
137
  { key: 'bypass', nameZh: '放行', description: '全部自动放行', available: true },
@@ -137,7 +139,6 @@ export class AgentRunner {
137
139
  { key: 'edit', nameZh: '编辑', description: '自动接受编辑,其他询问', available: true },
138
140
  { key: 'plan', nameZh: '规划', description: '只规划不执行', available: true },
139
141
  { key: 'noask', nameZh: '静默', description: '未批准则拒绝', available: true },
140
- { key: 'readonly', nameZh: '只读', description: '禁止修改项目文件,可在临时目录生成文件', available: true },
141
142
  ];
142
143
  }
143
144
  setPermissionGateway(gateway) {
@@ -77,7 +77,6 @@ export class CodexRunner {
77
77
  { key: 'bypass', nameZh: '放行', description: '全部自动(受 sandbox 约束)', available: true },
78
78
  { key: 'request', nameZh: '审批', description: '需要审批时询问', available: true },
79
79
  { key: 'noask', nameZh: '静默', description: '只执行已知安全操作', available: true },
80
- { key: 'readonly', nameZh: '只读', description: '禁止修改项目文件,可在临时目录生成文件', available: true },
81
80
  ];
82
81
  }
83
82
  setSendPrompt(_fn) { }
@@ -70,7 +70,6 @@ export class GeminiRunner {
70
70
  { key: 'edit', nameZh: '编辑', description: '仅 Claude 支持', available: false, unavailableReason: 'Gemini CLI 不支持此模式' },
71
71
  { key: 'plan', nameZh: '规划', description: 'Gemini 规划模式', available: true },
72
72
  { key: 'noask', nameZh: '静默', description: '仅 Claude 支持', available: false, unavailableReason: 'Gemini CLI 不支持此模式' },
73
- { key: 'readonly', nameZh: '只读', description: '禁止修改项目文件,可在临时目录生成文件', available: true },
74
73
  ];
75
74
  }
76
75
  setSendPrompt(_fn) { }
@@ -121,6 +120,9 @@ export class GeminiRunner {
121
120
  if (this.currentMode === 'plan') {
122
121
  args.push('--approval-mode=plan');
123
122
  }
123
+ else if (this.currentMode === 'noask') {
124
+ args.push('--approval-mode=default');
125
+ }
124
126
  else {
125
127
  args.push('--yolo');
126
128
  }
@@ -1,4 +1,4 @@
1
- import { AUNClient, GatewayDiscovery } from '@agentunion/fastaun';
1
+ import { AUNClient, GatewayDiscovery, E2EEError } from '@agentunion/fastaun';
2
2
  import crypto from 'crypto';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
@@ -120,6 +120,16 @@ export class AUNChannel {
120
120
  if (messageId)
121
121
  this.messageSeqMap.delete(messageId);
122
122
  }
123
+ shouldEncrypt(peerId) {
124
+ const cached = this.peerE2ee.get(peerId);
125
+ if (!cached)
126
+ return true;
127
+ if (Date.now() - cached.ts > AUNChannel.E2EE_PROBE_TTL) {
128
+ this.peerE2ee.delete(peerId);
129
+ return true;
130
+ }
131
+ return cached.ok;
132
+ }
123
133
  _aid;
124
134
  _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
125
135
  _chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
@@ -127,6 +137,9 @@ export class AUNChannel {
127
137
  peerInfoCache = new Map();
128
138
  messageSeqMap = new Map(); // messageId → seq (for ack)
129
139
  sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
140
+ peerE2ee = new Map();
141
+ static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
142
+ plaintextRecv = 0;
130
143
  // Reconnect state (TS-layer fallback, on top of SDK auto_reconnect)
131
144
  intentionalDisconnect = false;
132
145
  reconnectAttempt = 0;
@@ -161,7 +174,7 @@ export class AUNChannel {
161
174
  this.client = null;
162
175
  }
163
176
  this.connected = false;
164
- const aunPath = this.config.keystorePath || `${process.env.HOME || '~'}/.aun`;
177
+ const aunPath = this.config.keystorePath || path.join(os.homedir(), '.aun');
165
178
  const aidName = this.config.aid;
166
179
  const encryptionSeed = this.config.encryptionSeed || process.env.AUN_ENCRYPTION_SEED || undefined;
167
180
  // Gateway URL 解析:优先用配置的 gatewayUrl,否则通过 well-known 自动发现
@@ -185,12 +198,12 @@ export class AUNChannel {
185
198
  logger.info(`[AUN] Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
186
199
  // Create client with FileSecretStore (AES-256-GCM)
187
200
  // 不传 encryption_seed 时,SDK 自动从 {aun_path}/.seed 文件派生密钥(与 aun_cli.py 对齐)
188
- const rootCaPath = `${aunPath}/CA/root/root.crt`;
201
+ const rootCaPath = path.join(aunPath, 'CA', 'root', 'root.crt');
189
202
  this.client = new AUNClient({
190
203
  aun_path: aunPath,
191
204
  root_ca_path: rootCaPath,
192
205
  ...(encryptionSeed && { encryption_seed: encryptionSeed }),
193
- });
206
+ }, this.config.aunSdkLog ?? true);
194
207
  // Set gateway URL (internal property, same as Python SDK)
195
208
  this.client._gatewayUrl = gateway;
196
209
  // Register event handlers before connecting
@@ -226,6 +239,16 @@ export class AUNChannel {
226
239
  }
227
240
  }
228
241
  });
242
+ this.client.on('message.undecryptable', (data) => {
243
+ this.trace('IN', 'message.undecryptable', data);
244
+ const d = data;
245
+ logger.warn(`[AUN] Message undecryptable: from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
246
+ });
247
+ this.client.on('group.message_undecryptable', (data) => {
248
+ this.trace('IN', 'group.message_undecryptable', data);
249
+ const d = data;
250
+ logger.warn(`[AUN] Group message undecryptable: group=${d.group_id} from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
251
+ });
229
252
  // Authenticate
230
253
  // Workaround: SDK 0.3.x _loadIdentityOrRaise doesn't set identity.aid from requested aid,
231
254
  // causing gateway "missing aid" error. Patch to backfill aid on loaded identity.
@@ -494,6 +517,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
494
517
  this.acknowledgeImmediately(messageId, seq);
495
518
  return;
496
519
  }
520
+ // E2EE 能力探测:收到加密消息则标记对端支持,明文则计数审计
521
+ const msgEncrypted = !!(msg.e2ee);
522
+ if (fromAid) {
523
+ if (msgEncrypted) {
524
+ this.peerE2ee.set(fromAid, { ok: true, ts: Date.now() });
525
+ }
526
+ else {
527
+ this.plaintextRecv++;
528
+ }
529
+ }
497
530
  // Detect @mentions
498
531
  const mentions = [];
499
532
  if (this._aid && text.includes(`@${this._aid}`)) {
@@ -566,6 +599,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
566
599
  this.acknowledgeImmediately(messageId, seq);
567
600
  return;
568
601
  }
602
+ // E2EE 能力探测:收到加密群消息则标记发送者支持
603
+ const msgEncrypted = !!(msg.e2ee);
604
+ if (senderAid) {
605
+ if (msgEncrypted) {
606
+ this.peerE2ee.set(senderAid, { ok: true, ts: Date.now() });
607
+ }
608
+ else {
609
+ this.plaintextRecv++;
610
+ }
611
+ }
569
612
  // dispatch_mode from server tells agent how to work in this group
570
613
  const dispatchMode = msg.dispatch_mode ?? payload?.dispatch_mode ?? 'mention';
571
614
  const mentionedSelf = this._aid
@@ -748,29 +791,68 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
748
791
  const payload = { type: 'text', text: finalText };
749
792
  if (context?.threadId)
750
793
  payload.thread_id = context.threadId;
794
+ if (context?.metadata?.taskId)
795
+ payload.task_id = context.metadata.taskId;
796
+ if (context?.metadata?.chatmode)
797
+ payload.chatmode = context.metadata.chatmode;
751
798
  const isGroup = this.isGroupId(channelId);
752
- const params = { payload, encrypt: !isGroup };
753
799
  // Multi-instance routing: channelId may be "aid:device_id:slot_id"
754
800
  const colonIdx = channelId.indexOf(':');
755
801
  const targetAid = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
756
802
  if (colonIdx > 0) {
757
- params.payload.chat_id = channelId;
803
+ payload.chat_id = channelId;
758
804
  }
805
+ const encryptTarget = isGroup ? channelId : targetAid;
806
+ const encrypt = this.shouldEncrypt(encryptTarget);
807
+ const params = { payload, encrypt };
759
808
  try {
760
809
  if (isGroup) {
761
810
  params.group_id = channelId;
762
811
  this.trace('OUT', 'group.send', params);
763
- await this.client.call('group.send', params);
812
+ const result = await this.client.call('group.send', params);
813
+ if (!result || !result.message_id) {
814
+ logger.warn(`[AUN] group.send returned no message_id: ${JSON.stringify(result)}`);
815
+ }
764
816
  }
765
817
  else {
766
818
  params.to = targetAid;
767
819
  this.trace('OUT', 'message.send', params);
768
- await this.client.call('message.send', params);
820
+ const result = await this.client.call('message.send', params);
821
+ if (!result || !result.message_id) {
822
+ logger.warn(`[AUN] message.send returned no message_id: ${JSON.stringify(result)}`);
823
+ }
769
824
  }
770
825
  }
771
826
  catch (e) {
772
- this.trace('OUT', 'send.error', { channelId, error: String(e) });
773
- logger.error(`[AUN] Send failed to ${channelId}: ${e}`);
827
+ if (encrypt && e instanceof E2EEError) {
828
+ this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
829
+ logger.warn(`[AUN] E2EE send failed to ${channelId}, retrying plaintext: ${e}`);
830
+ params.encrypt = false;
831
+ try {
832
+ if (isGroup) {
833
+ this.trace('OUT', 'group.send.fallback', params);
834
+ const result = await this.client.call('group.send', params);
835
+ if (!result || !result.message_id) {
836
+ logger.warn(`[AUN] group.send fallback returned no message_id: ${JSON.stringify(result)}`);
837
+ }
838
+ }
839
+ else {
840
+ this.trace('OUT', 'message.send.fallback', params);
841
+ const result = await this.client.call('message.send', params);
842
+ if (!result || !result.message_id) {
843
+ logger.warn(`[AUN] message.send fallback returned no message_id: ${JSON.stringify(result)}`);
844
+ }
845
+ }
846
+ }
847
+ catch (e2) {
848
+ this.trace('OUT', 'send.fallback.error', { channelId, error: String(e2) });
849
+ logger.error(`[AUN] Plaintext fallback also failed to ${channelId}: ${e2}`);
850
+ }
851
+ }
852
+ else {
853
+ this.trace('OUT', 'send.error', { channelId, error: String(e) });
854
+ logger.error(`[AUN] Send failed to ${channelId}: ${e}`);
855
+ }
774
856
  }
775
857
  }
776
858
  /**
@@ -795,15 +877,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
795
877
  encrypt: true,
796
878
  };
797
879
  try {
880
+ const stage = payload?.stage ?? 'unknown';
798
881
  if (this.isGroupId(channelId)) {
799
882
  params.group_id = targetId;
800
883
  this.trace('OUT', 'group.thought.put', params);
801
884
  await this.client.call('group.thought.put', params);
885
+ logger.debug(`[AUN] thought.put ok group=${targetId} task=${taskId} stage=${stage}`);
802
886
  }
803
887
  else {
804
888
  params.to = targetId;
805
889
  this.trace('OUT', 'message.thought.put', params);
806
890
  await this.client.call('message.thought.put', params);
891
+ logger.debug(`[AUN] thought.put ok p2p=${targetId} task=${taskId} stage=${stage}`);
807
892
  }
808
893
  }
809
894
  catch (e) {
@@ -890,23 +975,61 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
890
975
  };
891
976
  if (context?.threadId)
892
977
  filePayload.thread_id = context.threadId;
978
+ if (context?.metadata?.taskId)
979
+ filePayload.task_id = context.metadata.taskId;
980
+ if (context?.metadata?.chatmode)
981
+ filePayload.chatmode = context.metadata.chatmode;
893
982
  const isGroup = this.isGroupId(channelId);
894
- const params = { payload: filePayload, encrypt: !isGroup };
895
983
  // Multi-instance routing
896
984
  const fileColonIdx = channelId.indexOf(':');
897
985
  const fileTargetAid = fileColonIdx > 0 ? channelId.substring(0, fileColonIdx) : channelId;
898
986
  if (fileColonIdx > 0) {
899
- params.payload.chat_id = channelId;
987
+ filePayload.chat_id = channelId;
900
988
  }
901
- if (isGroup) {
902
- params.group_id = channelId;
903
- this.trace('OUT', 'group.send.file', params);
904
- await this.client.call('group.send', params);
989
+ const encryptTarget = isGroup ? channelId : fileTargetAid;
990
+ const encrypt = this.shouldEncrypt(encryptTarget);
991
+ const params = { payload: filePayload, encrypt };
992
+ try {
993
+ if (isGroup) {
994
+ params.group_id = channelId;
995
+ this.trace('OUT', 'group.send.file', params);
996
+ const result = await this.client.call('group.send', params);
997
+ if (!result || !result.message_id) {
998
+ logger.warn(`[AUN] group.send.file returned no message_id: ${JSON.stringify(result)}`);
999
+ }
1000
+ }
1001
+ else {
1002
+ params.to = fileTargetAid;
1003
+ this.trace('OUT', 'message.send.file', params);
1004
+ const result = await this.client.call('message.send', params);
1005
+ if (!result || !result.message_id) {
1006
+ logger.warn(`[AUN] message.send.file returned no message_id: ${JSON.stringify(result)}`);
1007
+ }
1008
+ }
905
1009
  }
906
- else {
907
- params.to = fileTargetAid;
908
- this.trace('OUT', 'message.send.file', params);
909
- await this.client.call('message.send', params);
1010
+ catch (sendErr) {
1011
+ if (encrypt && sendErr instanceof E2EEError) {
1012
+ this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
1013
+ logger.warn(`[AUN] E2EE sendFile failed to ${channelId}, retrying plaintext: ${sendErr}`);
1014
+ params.encrypt = false;
1015
+ if (isGroup) {
1016
+ this.trace('OUT', 'group.send.file.fallback', params);
1017
+ const result = await this.client.call('group.send', params);
1018
+ if (!result || !result.message_id) {
1019
+ logger.warn(`[AUN] group.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
1020
+ }
1021
+ }
1022
+ else {
1023
+ this.trace('OUT', 'message.send.file.fallback', params);
1024
+ const result = await this.client.call('message.send', params);
1025
+ if (!result || !result.message_id) {
1026
+ logger.warn(`[AUN] message.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
1027
+ }
1028
+ }
1029
+ }
1030
+ else {
1031
+ throw sendErr;
1032
+ }
910
1033
  }
911
1034
  logger.info(`[AUN] File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
912
1035
  }
@@ -941,27 +1064,41 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
941
1064
  if (context?.threadId)
942
1065
  payload.thread_id = context.threadId;
943
1066
  const isGroup = this.isGroupId(channelId);
944
- const params = { payload, encrypt: !isGroup };
945
1067
  // Multi-instance routing
946
1068
  const statusColonIdx = channelId.indexOf(':');
947
1069
  const statusTargetAid = statusColonIdx > 0 ? channelId.substring(0, statusColonIdx) : channelId;
948
1070
  if (statusColonIdx > 0) {
949
1071
  payload.chat_id = channelId;
950
1072
  }
1073
+ const encryptTarget = isGroup ? channelId : statusTargetAid;
1074
+ const encrypt = this.shouldEncrypt(encryptTarget);
1075
+ const params = { payload, encrypt };
1076
+ const sendWithFallback = (method) => {
1077
+ this.client.call(method, params).catch(e => {
1078
+ if (encrypt && e instanceof E2EEError) {
1079
+ this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
1080
+ logger.warn(`[AUN] E2EE status send failed to ${channelId}, retrying plaintext`);
1081
+ params.encrypt = false;
1082
+ this.client.call(method, params).catch(e2 => {
1083
+ logger.debug(`[AUN] Processing status fallback failed: ${e2}`);
1084
+ });
1085
+ }
1086
+ else {
1087
+ logger.debug(`[AUN] Processing status failed: ${e}`);
1088
+ }
1089
+ });
1090
+ };
951
1091
  if (isGroup) {
952
1092
  params.group_id = channelId;
953
1093
  this.trace('OUT', 'group.send.status', params);
954
- this.client.call('group.send', params).catch(e => {
955
- logger.debug(`[AUN] Processing status failed: ${e}`);
956
- });
1094
+ sendWithFallback('group.send');
957
1095
  }
958
1096
  else {
959
1097
  params.to = statusTargetAid;
960
1098
  this.trace('OUT', 'message.send.status', params);
961
- this.client.call('message.send', params).catch(e => {
962
- logger.debug(`[AUN] Processing status failed: ${e}`);
963
- });
1099
+ sendWithFallback('message.send');
964
1100
  }
1101
+ logger.info(`[AUN] task.${status} task=${taskId} session=${sessionId} target=${channelId}`);
965
1102
  }
966
1103
  sendCustomPayload(channelId, payload) {
967
1104
  if (!this.client || !this.connected)
@@ -1068,6 +1205,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1068
1205
  aid: this._aid,
1069
1206
  reconnectAttempt: this.reconnectAttempt,
1070
1207
  maxAttempts: AUNChannel.RECONNECT_DELAYS.length,
1208
+ plaintextRecv: this.plaintextRecv,
1071
1209
  };
1072
1210
  }
1073
1211
  /** 读取本地 agent.md 中的 name(用于身份上下文展示) */
@@ -1150,6 +1288,7 @@ export class AUNChannelPlugin {
1150
1288
  encryptionSeed: inst.encryptionSeed,
1151
1289
  owner: inst.owner,
1152
1290
  aunTrace: config.debug?.aunTrace,
1291
+ aunSdkLog: config.debug?.aunSdkLog,
1153
1292
  });
1154
1293
  const adapter = {
1155
1294
  channelName: inst.name,