evolcore 0.0.12 → 0.0.14

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 (116) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/ec-safe-output.js +26 -7
  4. package/bin/install-codex-managed-hooks.mjs +201 -0
  5. package/dist/agents/claude-runner.js +105 -11
  6. package/dist/agents/codex-app-server-client.js +176 -7
  7. package/dist/agents/codex-runner.js +248 -84
  8. package/dist/agents/ecagent-runner.js +21 -2
  9. package/dist/agents/gemini-runner.js +9 -4
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/store.js +13 -7
  12. package/dist/aun/group-fs-download.js +56 -0
  13. package/dist/aun/group-identity.js +1 -2
  14. package/dist/aun/msg/group.js +11 -33
  15. package/dist/aun/msg/index.js +1 -2
  16. package/dist/aun/msg/managed-operation.js +176 -55
  17. package/dist/aun/msg/upload.js +16 -1
  18. package/dist/aun/outbox.js +31 -2
  19. package/dist/aun/rpc/client.js +66 -0
  20. package/dist/aun/rpc/index.js +1 -2
  21. package/dist/aun/storage/{upload.js → client.js} +11 -2
  22. package/dist/aun/storage/index.js +1 -2
  23. package/dist/channels/aun.js +514 -150
  24. package/dist/channels/daemon.js +2 -0
  25. package/dist/cli/agent.js +9 -96
  26. package/dist/cli/aun-commands.js +1 -1
  27. package/dist/cli/cli-argv.js +10 -0
  28. package/dist/cli/config.js +9 -17
  29. package/dist/cli/ctl-command.js +1 -1
  30. package/dist/cli/daemon-commands.js +39 -24
  31. package/dist/cli/fs-command.js +65 -19
  32. package/dist/cli/init.js +77 -8
  33. package/dist/cli/queue-command.js +42 -0
  34. package/dist/cli/restart-monitor.js +3 -14
  35. package/dist/cli/task-context.js +172 -0
  36. package/dist/cli/trigger-command.js +7 -0
  37. package/dist/config/access-policy.js +1 -2
  38. package/dist/config/builtin-roles.js +7 -0
  39. package/dist/config/config-manager.js +27 -3
  40. package/dist/config/contact-book-store.js +7 -2
  41. package/dist/config/peer-role-resolver.js +6 -2
  42. package/dist/config/role-migration-startup.js +53 -0
  43. package/dist/config/role-store.js +1 -1
  44. package/dist/core/auth/agent-delegation.js +1 -1
  45. package/dist/core/auth/auth-gateway.js +30 -1
  46. package/dist/core/auth/authorization-audit.js +47 -0
  47. package/dist/core/auth/operation-authorizer.js +1 -0
  48. package/dist/core/auth/operation-catalog.js +10 -1
  49. package/dist/core/bootstrap-messages.js +9 -2
  50. package/dist/core/bootstrap-service.js +8 -1
  51. package/dist/core/command/command-handler.js +201 -51
  52. package/dist/core/command/menu-catalog.js +4 -0
  53. package/dist/core/command/menu-handler.js +85 -18
  54. package/dist/core/command/menu-protocol.js +23 -2
  55. package/dist/core/command/slash-handler.js +58 -10
  56. package/dist/core/event-catalog.js +8 -0
  57. package/dist/core/handoff/runtime.js +12 -1
  58. package/dist/core/handoff/store.js +5 -1
  59. package/dist/core/interaction-router.js +10 -0
  60. package/dist/core/message/im-renderer.js +34 -1
  61. package/dist/core/message/message-bridge.js +68 -13
  62. package/dist/core/message/message-log.js +2 -0
  63. package/dist/core/message/message-queue.js +267 -39
  64. package/dist/core/message/message-utils.js +81 -4
  65. package/dist/core/message/response-engine.js +242 -48
  66. package/dist/core/message/stream-debouncer.js +10 -2
  67. package/dist/core/permission/approval-gateway.js +1 -0
  68. package/dist/core/permission/ec-command-parser.js +649 -18
  69. package/dist/core/permission/index.js +1 -1
  70. package/dist/core/permission/mode.js +15 -0
  71. package/dist/core/permission/readonly-shell-query.js +148 -8
  72. package/dist/core/permission/sandbox-runtime.js +21 -0
  73. package/dist/core/permission/tool-policy.js +266 -56
  74. package/dist/core/protected-paths.js +107 -2
  75. package/dist/core/runtime-lock.js +101 -0
  76. package/dist/core/session/session-manager.js +21 -2
  77. package/dist/core/session/session-mapper.js +20 -1
  78. package/dist/eck/detect.js +18 -1
  79. package/dist/eck/group-rules-sync.js +15 -44
  80. package/dist/index.js +191 -89
  81. package/dist/ipc.js +16 -3
  82. package/dist/paths.js +3 -0
  83. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  84. package/dist/response-system/modes/single-session/index.js +3 -0
  85. package/dist/trigger/feedback.js +64 -11
  86. package/dist/trigger/history.js +42 -7
  87. package/dist/trigger/manager.js +21 -1
  88. package/dist/trigger/parser.js +14 -2
  89. package/dist/trigger/scheduler.js +1 -0
  90. package/dist/trigger/validation.js +67 -6
  91. package/dist/utils/ecweb-supervisor.js +332 -0
  92. package/dist/utils/error-utils.js +17 -0
  93. package/dist/utils/logger.js +37 -4
  94. package/kits/docs/evolcore/trigger.md +17 -1
  95. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  96. package/kits/schemas/role-config.schema.1.json +1 -0
  97. package/kits/templates/roles/admin.json +7 -0
  98. package/kits/templates/roles/member.json +8 -1
  99. package/kits/templates/roles/visitor.json +2 -0
  100. package/package.json +1 -1
  101. package/dist/aun/msg/payload-type.js +0 -27
  102. package/dist/aun/rpc/caller.js +0 -42
  103. package/dist/aun/rpc/connection.js +0 -25
  104. package/dist/aun/storage/manage.js +0 -10
  105. package/dist/config/access-policy-domain.js +0 -18
  106. package/dist/config/config-batch-get.js +0 -11
  107. package/dist/config/owner-policy.js +0 -4
  108. package/dist/config/role-config-v4-startup.js +0 -32
  109. package/dist/config/role-config-v5-startup.js +0 -27
  110. package/dist/core/auth/trigger-authorization.js +0 -15
  111. package/dist/core/interaction-registration.js +0 -10
  112. package/dist/core/permission/execution-sandbox.js +0 -16
  113. package/dist/core/relation/peer-key.js +0 -1
  114. package/dist/core/session/session-key.js +0 -24
  115. package/dist/eck/baseagent-caps.js +0 -18
  116. package/dist/eck/rules-loader.js +0 -28
@@ -10,6 +10,7 @@ import { normalizePermissionMode } from '../core/permission/mode.js';
10
10
  import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
11
11
  import { logger } from '../utils/logger.js';
12
12
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
13
+ import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
13
14
  import { resolveEcagentConfig } from './baseagent.js';
14
15
  import { buildModelRequestHeaders } from './request-identity.js';
15
16
  const PROVIDER_ID = 'evolcore-gateway';
@@ -959,6 +960,21 @@ export class EcagentRunner {
959
960
  async authorizeTool(sessionId, projectPath, mode, toolName, input) {
960
961
  const permissionContext = this.permissionContexts.get(sessionId);
961
962
  const recordBlockedOperation = async (policyCode, operationInput = input) => {
963
+ const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
964
+ auditToolPreflightDenial({
965
+ toolName,
966
+ policyCode,
967
+ reason: 'policy denied',
968
+ summary,
969
+ sessionId,
970
+ agentAid: permissionContext?.selfAid,
971
+ permissionMode: mode,
972
+ channel: permissionContext?.channel,
973
+ actorId: permissionContext?.userId,
974
+ role: permissionContext?.role,
975
+ selfAid: permissionContext?.selfAid,
976
+ taskId: permissionContext?.taskId,
977
+ });
962
978
  try {
963
979
  await permissionContext?.recordExecutionAnomaly?.({
964
980
  code: 'operation_blocked',
@@ -967,7 +983,10 @@ export class EcagentRunner {
967
983
  occurredAt: Date.now(),
968
984
  toolName,
969
985
  policyCode,
970
- summary: summarizeToolInputForAudit(toolName, operationInput).slice(0, 512),
986
+ decisionSource: 'policy',
987
+ agentAid: permissionContext?.selfAid,
988
+ permissionMode: mode,
989
+ summary,
971
990
  effect: 'operation_skipped',
972
991
  });
973
992
  }
@@ -1003,7 +1022,7 @@ export class EcagentRunner {
1003
1022
  sessionId, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
1004
1023
  });
1005
1024
  if (decision.behavior === 'deny') {
1006
- await recordBlockedOperation('readonly_mode', checkedInput);
1025
+ await recordBlockedOperation(decision.policyCode ?? 'readonly_mode', checkedInput);
1007
1026
  return { block: true, reason: decision.message };
1008
1027
  }
1009
1028
  return {};
@@ -279,11 +279,16 @@ export class GeminiRunner {
279
279
  const requestedPermissionMode = modelOverride?.permissionMode || this.currentMode;
280
280
  const permissionProfile = resolveGeminiPermissionProfile(requestedPermissionMode);
281
281
  const callModel = modelOverride?.model || this.model;
282
+ const runtimeDir = runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR;
283
+ const managedTmpDir = runtimeDir ?? process.env.TMPDIR;
284
+ if (!managedTmpDir || !path.isAbsolute(managedTmpDir))
285
+ throw new Error('managed TMPDIR is unset or not absolute');
286
+ const runtimeLockDir = runtimeEnv?.EVOLCORE_RUNTIME_LOCK_DIR;
282
287
  const geminiHome = path.join(os.homedir(), '.gemini');
283
288
  fs.mkdirSync(geminiHome, { recursive: true, mode: 0o700 });
284
289
  const sandboxProbe = buildBubblewrapCommand(this.resolved.cliPath, [], {
285
290
  projectPath,
286
- writablePaths: [geminiHome],
291
+ writablePaths: [geminiHome, ...(runtimeDir ? [runtimeDir] : []), ...(runtimeLockDir ? [runtimeLockDir] : [])],
287
292
  });
288
293
  if (!sandboxProbe && workspaceContainsHClassPaths(projectPath)) {
289
294
  throw new Error('Gemini 缺少可用的路径级隔离运行时,且项目覆盖 EvolCore 受保护根;已拒绝启动本轮任务');
@@ -303,13 +308,13 @@ export class GeminiRunner {
303
308
  if (hasStandardGeminiAdminPolicy()) {
304
309
  throw new Error('Gemini 系统级 admin policy 会忽略 EvolCore 的临时安全策略,已拒绝启动本轮任务');
305
310
  }
306
- const policyPath = path.join(os.tmpdir(), `evolcore-gemini-permission-${crypto.randomUUID()}.toml`);
311
+ const policyPath = path.join(managedTmpDir, `evolcore-gemini-permission-${crypto.randomUUID()}.toml`);
307
312
  fs.writeFileSync(policyPath, buildGeminiAdminPolicy(permissionProfile), { mode: 0o600, flag: 'wx' });
308
313
  tempFiles.push(policyPath);
309
314
  args.push(...buildGeminiPermissionArgs(permissionProfile, policyPath, !!sandboxProbe));
310
315
  }
311
316
  if (images?.length) {
312
- const tmpDir = os.tmpdir();
317
+ const tmpDir = managedTmpDir;
313
318
  const fileParts = [];
314
319
  for (let i = 0; i < images.length; i++) {
315
320
  const img = images[i];
@@ -337,7 +342,7 @@ export class GeminiRunner {
337
342
  const env = this.buildAgentEnv(sessionId, runtimeEnv);
338
343
  const sandboxedCommand = buildBubblewrapCommand(this.resolved.cliPath, args, {
339
344
  projectPath,
340
- writablePaths: [geminiHome],
345
+ writablePaths: [geminiHome, ...(runtimeDir ? [runtimeDir] : []), ...(runtimeLockDir ? [runtimeLockDir] : [])],
341
346
  readonlyPaths: tempFiles,
342
347
  });
343
348
  const child = spawn(sandboxedCommand?.command ?? this.resolved.cliPath, sandboxedCommand?.args ?? args, {
@@ -62,7 +62,8 @@ async function candidateExists(store, candidate) {
62
62
  throw new Error(`Gateway 不可达,无法查重控制 AID:${r.error?.message ?? 'unknown'}`);
63
63
  }
64
64
  /**
65
- * 生成控制 AID:循环候选 → candidateExists 查重(权威 PKI 判据)→ 不冲突则 aidCreate
65
+ * 生成控制 AID:循环候选 → candidateExists 查重(权威 PKI 判据)→ 不冲突则 aidCreate
66
+ * 查重通过但注册失败时也换候选重试,覆盖查重与注册之间的竞态。
66
67
  * - 查重走 GET 证书(见 candidateExists;不拉 agent.md,控制 AID 本就不传 agent.md)
67
68
  * - fail-fast:查重探测失败(网关不可达)立即抛错,不掩盖成"均冲突"
68
69
  * - agent.md 不上传:aidCreate 仅注册身份 + 写私钥,不调 agentmdPut
@@ -72,6 +73,7 @@ export async function generateControlAid(aidDomain) {
72
73
  ? resolveControlAidDomain()
73
74
  : normalizeAidDomain(aidDomain, 'aidDomain');
74
75
  const store = await getAidStore({ slotId: SLOT.cli });
76
+ let lastRegistrationError;
75
77
  try {
76
78
  for (let i = 0; i < MAX_ATTEMPTS; i++) {
77
79
  const candidate = candidateAid(finalAidDomain);
@@ -79,22 +81,43 @@ export async function generateControlAid(aidDomain) {
79
81
  logger.info(`[control-aid] ${candidate} 已注册,重试 (${i + 1}/${MAX_ATTEMPTS})`);
80
82
  continue;
81
83
  }
82
- const created = await aidCreate(candidate);
83
- // 清理 aidCreate 内部另开的 client/store——关闭失败不可丢弃已注册的 AID(否则下次 init
84
- // 会把它当冲突,白白消耗一次重试)。close 异常降级为 warn。
85
84
  try {
86
- await created.client?.close?.();
85
+ const created = await aidCreate(candidate);
86
+ // 清理 aidCreate 内部另开的 client/store——关闭失败不可丢弃已注册的 AID(否则下次 init
87
+ // 会把它当冲突,白白消耗一次重试)。close 异常降级为 warn。
88
+ try {
89
+ await created.client?.close?.();
90
+ }
91
+ catch (e) {
92
+ logger.warn(`[control-aid] client.close() 失败(非致命): ${e}`);
93
+ }
94
+ try {
95
+ await created.store?.close?.();
96
+ }
97
+ catch (e) {
98
+ logger.warn(`[control-aid] store.close() 失败(非致命): ${e}`);
99
+ }
100
+ return { aid: created.aid, gateway: created.gateway };
87
101
  }
88
102
  catch (e) {
89
- logger.warn(`[control-aid] client.close() 失败(非致命): ${e}`);
103
+ // 查重与实际注册之间存在 TOCTOU 竞态;注册接口也可能因服务端暂时失败而拒绝,
104
+ // 这些情况都换一个候选继续尝试。查重阶段的网关错误仍在 candidateExists 中 fail-fast。
105
+ lastRegistrationError = e;
106
+ const reason = e instanceof Error ? e.message : String(e);
107
+ logger.warn(`[control-aid] ${candidate} 注册失败: ${reason},重试 (${i + 1}/${MAX_ATTEMPTS})`);
90
108
  }
91
- try {
92
- await created.store?.close?.();
93
- }
94
- catch (e) {
95
- logger.warn(`[control-aid] store.close() 失败(非致命): ${e}`);
96
- }
97
- return { aid: created.aid, gateway: created.gateway };
109
+ }
110
+ if (lastRegistrationError !== undefined) {
111
+ const reason = lastRegistrationError instanceof Error
112
+ ? lastRegistrationError.message
113
+ : String(lastRegistrationError);
114
+ const error = new Error(`无法生成控制 AID:连续 ${MAX_ATTEMPTS} 次注册尝试失败;最后错误:${reason}`, {
115
+ cause: lastRegistrationError,
116
+ });
117
+ const code = lastRegistrationError?.code;
118
+ if (typeof code === 'string')
119
+ error.code = code;
120
+ throw error;
98
121
  }
99
122
  throw new Error(`无法生成控制 AID:连续 ${MAX_ATTEMPTS} 次候选均冲突`);
100
123
  }
@@ -7,21 +7,27 @@ import { AUN_KEYSTORE_ENCRYPTION_SEED } from './encryption-seed-policy.js';
7
7
  * slotIsolationKey 取第一个分隔符(空格/'/'/':')之前的部分作为隔离键,
8
8
  * 隔离键相同 = 共享消费通道(token / seq 游标 / 消息过滤)。
9
9
  *
10
- * 'evolcore daemon' → 隔离键 'evolcore'
11
- * 'evolcore cli' → 隔离键 'evolcore' ├ 共享 evolcore 消费通道
12
- * 'evolcore netcheck' → 隔离键 'evolcore'
10
+ * 'evolcore daemon' → 隔离键 'evolcore'
11
+ * 'evolcore-cli' → 隔离键 'evolcore-cli'
12
+ * 'evolcore-netcheck' → 隔离键 'evolcore-netcheck'
13
+ * 'evolcore-group-rules' → 隔离键 'evolcore-group-rules'
13
14
  * 'evolcore-bench' → 隔离键 'evolcore-bench'(连字符非分隔符)→ 独立通道
14
15
  *
15
16
  * 设计意图:
16
- * - daemon 长连接 + cli 短连接共存于 evolcore 通道(1 长 + N 短,短连接不踢长连接)
17
- * - netcheck 长连接抢 evolcore 长连接位 踢掉 daemon(用于踢人/extra_info 测试)
17
+ * - daemon 保留历史 slot 值;slot_id 是网关上的消费游标身份,不可改名。
18
+ * - CLI / ecweb 查询使用新的独立 slot,不能共享 daemon 的 token 或 seq 游标。
19
+ * - netcheck 使用独立消费者,不得因诊断连接影响 daemon 的入站顺序。
20
+ * - 群规则同步是短连接 RPC,也必须使用独立 slot,避免触碰 daemon 消费游标。
18
21
  * - bench 各并发单元用 evolcore-bench-<N>,各自独立隔离键,互不干扰
19
22
  */
20
23
  export const SLOT = {
24
+ // Wire-compatibility contract: changing this creates a new gateway cursor
25
+ // and can replay retained history into the daemon.
21
26
  daemon: 'evolcore daemon',
22
- cli: 'evolcore cli',
27
+ cli: 'evolcore-cli',
28
+ groupRules: 'evolcore-group-rules',
23
29
  bench: 'evolcore-bench',
24
- netcheck: 'evolcore netcheck',
30
+ netcheck: 'evolcore-netcheck',
25
31
  };
26
32
  /** 加载身份失败时抛出,携带 SDK 的错误码与消息。 */
27
33
  export class AidLoadError extends Error {
@@ -0,0 +1,56 @@
1
+ import { createHash } from 'node:crypto';
2
+ export class GroupFsDownloadError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = 'GroupFsDownloadError';
8
+ }
9
+ }
10
+ /** Download a group file into memory through the ticket/data plane. */
11
+ export async function downloadGroupFsBytes(client, path) {
12
+ const ticketValue = await client.call('group.fs.create_download_ticket', { path });
13
+ if (!isRecord(ticketValue)) {
14
+ throw new GroupFsDownloadError('INVALID_TICKET', 'group.fs.create_download_ticket returned invalid response');
15
+ }
16
+ const downloadUrl = stringField(ticketValue, ['download_url', 'url']);
17
+ if (!downloadUrl) {
18
+ throw new GroupFsDownloadError('MISSING_DOWNLOAD_URL', 'group.fs.create_download_ticket did not return download_url');
19
+ }
20
+ const bytes = await client.group.fs.lowlevel.httpGet(downloadUrl, bearerHeaders(client));
21
+ const expectedSha = stringField(ticketValue, ['sha256']);
22
+ if (expectedSha && sha256Hex(bytes).toLowerCase() !== expectedSha.toLowerCase()) {
23
+ throw new GroupFsDownloadError('HASH_MISMATCH', 'download hash verification failed');
24
+ }
25
+ return { bytes, ticket: ticketValue };
26
+ }
27
+ function isRecord(value) {
28
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
29
+ }
30
+ function stringField(value, keys) {
31
+ for (const key of keys) {
32
+ const field = value[key];
33
+ if (typeof field === 'string' && field.trim())
34
+ return field;
35
+ }
36
+ return undefined;
37
+ }
38
+ function sha256Hex(bytes) {
39
+ return createHash('sha256').update(bytes).digest('hex');
40
+ }
41
+ function bearerHeaders(client) {
42
+ const anyClient = client;
43
+ if (typeof anyClient.getAccessToken === 'function') {
44
+ const token = stringValue(anyClient.getAccessToken());
45
+ if (token)
46
+ return { Authorization: `Bearer ${token}` };
47
+ }
48
+ const token = stringValue(anyClient.accessToken)
49
+ || stringValue(anyClient.access_token)
50
+ || stringValue(anyClient._identity?.access_token)
51
+ || stringValue(anyClient._sessionParams?.access_token);
52
+ return token ? { Authorization: `Bearer ${token}` } : undefined;
53
+ }
54
+ function stringValue(value) {
55
+ return value === undefined || value === null ? '' : String(value).trim();
56
+ }
@@ -5,6 +5,5 @@ export function isExplicitGroupId(value) {
5
5
  return (id.startsWith('group.') && id.includes('/'))
6
6
  || /^\d+\.[a-z0-9.-]+$/i.test(id)
7
7
  || /^grp_[a-z0-9_-]+$/i.test(id)
8
- || /^g-[a-z0-9_-]+(?:[.@][a-z0-9.-]+)?$/i.test(id)
9
- || /(?:^|\.)groupid\.pub$/i.test(id);
8
+ || /^g-[a-z0-9_-]+(?:[.@][a-z0-9.-]+)?$/i.test(id);
10
9
  }
@@ -14,6 +14,7 @@ import { ipcQuery } from '../../ipc.js';
14
14
  import { resolvePaths } from '../../paths.js';
15
15
  import { AGENT_DELEGATION_TOKEN_ENV } from '../../core/auth/agent-delegation.js';
16
16
  import { normalizeAunMentionEntries } from './mention-schema.js';
17
+ import { downloadGroupFsBytes } from '../group-fs-download.js';
17
18
  function buildGroupPayload(body) {
18
19
  return body.mode === 'text' ? { type: 'text', text: body.text } : { ...body.payload };
19
20
  }
@@ -130,17 +131,18 @@ async function daemonGroupSendResult(args, payload, runtimeContext, result) {
130
131
  status: result.status,
131
132
  };
132
133
  }
133
- if (!result.group_id || !result.message) {
134
+ const messageId = result.message_id
135
+ ?? (typeof result.message?.message_id === 'string' ? result.message.message_id : undefined);
136
+ if (!result.group_id || !messageId) {
134
137
  return { ok: false, error: 'daemon AUN group send returned no group message' };
135
138
  }
136
- const messageId = result.message_id
137
- ?? (typeof result.message.message_id === 'string' ? result.message.message_id : undefined);
138
139
  if (messageId && !result.log_written) {
139
140
  appendGroupSendOutboundLog(args, messageId, payload, runtimeContext, result.encrypt ?? resolveAunMessageEncrypt(args.encrypt));
140
141
  }
141
142
  return {
142
143
  ok: true,
143
144
  group_id: result.group_id || args.groupId,
145
+ message_id: messageId,
144
146
  message: result.message,
145
147
  event: result.event,
146
148
  };
@@ -1055,20 +1057,7 @@ async function uploadGroupRulesBytes(client, groupId, bytes) {
1055
1057
  }
1056
1058
  async function readGroupRulesBytes(client, groupId) {
1057
1059
  const remoteRef = `${groupId}:${GROUP_RULES_FILE_PATH}`;
1058
- const ticket = await client.call('group.fs.create_download_ticket', { path: remoteRef });
1059
- if (!isRecord(ticket)) {
1060
- throw new Error('group.fs.create_download_ticket returned invalid response');
1061
- }
1062
- const downloadUrl = stringField(ticket, ['download_url', 'url']);
1063
- if (!downloadUrl) {
1064
- throw new Error('group.fs.create_download_ticket did not return download_url');
1065
- }
1066
- const bytes = await client.group.fs.lowlevel.httpGet(downloadUrl, bearerHeaders(client));
1067
- const expectedSha = stringField(ticket, ['sha256']);
1068
- if (expectedSha && sha256Hex(bytes).toLowerCase() !== expectedSha.toLowerCase()) {
1069
- throw new Error('download hash verification failed');
1070
- }
1071
- return bytes;
1060
+ return (await downloadGroupFsBytes(client, remoteRef)).bytes;
1072
1061
  }
1073
1062
  function decodeRulesText(bytes) {
1074
1063
  return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
@@ -1126,22 +1115,6 @@ function groupIndexEtagFromUpdate(result, groupId) {
1126
1115
  function sha256Hex(bytes) {
1127
1116
  return createHash('sha256').update(bytes).digest('hex');
1128
1117
  }
1129
- function bearerHeaders(client) {
1130
- const token = accessTokenFromClient(client);
1131
- return token ? { Authorization: `Bearer ${token}` } : undefined;
1132
- }
1133
- function accessTokenFromClient(client) {
1134
- const anyClient = client;
1135
- if (typeof anyClient.getAccessToken === 'function') {
1136
- const token = stringValue(anyClient.getAccessToken());
1137
- if (token)
1138
- return token;
1139
- }
1140
- return stringValue(anyClient.accessToken)
1141
- || stringValue(anyClient.access_token)
1142
- || stringValue(anyClient._identity?.access_token)
1143
- || stringValue(anyClient._sessionParams?.access_token);
1144
- }
1145
1118
  function stringField(obj, keys) {
1146
1119
  for (const key of keys) {
1147
1120
  const value = obj[key];
@@ -1189,6 +1162,11 @@ function rulesFileStatusFromError(error) {
1189
1162
  if (tagged)
1190
1163
  return tagged;
1191
1164
  const haystack = errorHaystack(error);
1165
+ // A peer cache miss means signer discovery failed; it does not establish
1166
+ // that the group's rules file is absent.
1167
+ if (/peer\s+not\s+found\s+in\s+cache/i.test(haystack)) {
1168
+ return 'unreadable';
1169
+ }
1192
1170
  if (/(forbidden|unauthorized|permission|denied|no_permission|permission_denied|无权限|权限|拒绝|\b401\b|\b403\b)/i.test(haystack)) {
1193
1171
  return 'forbidden';
1194
1172
  }
@@ -1,5 +1,4 @@
1
1
  export { msgSend, msgPull, msgAck, msgRecall, msgOnline, } from './p2p.js';
2
2
  export { msgHistory, MAX_MSG_HISTORY_LIMIT } from './history.js';
3
3
  export { groupSend, groupPull, groupAck, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupSuspend, groupResume, groupJoin, groupLeave, groupInvite, groupKick, groupMembers, groupOnline, groupSetRole, groupTransferOwner, groupBan, groupUnban, groupBanlist, groupRules, groupUpdateRules, groupRulesFileGet, groupRulesFileSet, groupRulesFilePublish, } from './group.js';
4
- export { uploadFileAndBuildPayload } from './upload.js';
5
- export { inferPayloadType, isValidPayloadType } from './payload-type.js';
4
+ export { uploadFileAndBuildPayload, inferPayloadType, isValidPayloadType } from './upload.js';