evolcore 0.0.20 → 0.0.22

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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. package/dist/config/aun-gateway-config.js +0 -2
@@ -75,6 +75,33 @@ export function atomicWrite(filePath, content) {
75
75
  export function atomicWriteJson(filePath, value) {
76
76
  atomicWrite(filePath, JSON.stringify(value, null, 2) + '\n');
77
77
  }
78
+ /**
79
+ * Update a small protected file without replacing its directory entry.
80
+ *
81
+ * H-class sandbox mounts bind individual files by pathname. Replacing such a
82
+ * file with rename(2) changes the inode while bubblewrap is constructing its
83
+ * mount table and can make the bind/remount operation fail. This helper keeps
84
+ * the inode stable; callers should use it only for best-effort, daemon-owned
85
+ * state where a crash during the write may leave incomplete content.
86
+ */
87
+ export function stableWrite(filePath, content) {
88
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
89
+ const noFollow = fs.constants.O_NOFOLLOW ?? 0;
90
+ const fd = fs.openSync(filePath, fs.constants.O_RDWR | fs.constants.O_CREAT | noFollow, 0o600);
91
+ try {
92
+ const stat = fs.fstatSync(fd);
93
+ if (!stat.isFile())
94
+ throw new Error(`stable write target is not a regular file: ${filePath}`);
95
+ fs.ftruncateSync(fd, 0);
96
+ fs.writeFileSync(fd, content, 'utf8');
97
+ }
98
+ finally {
99
+ fs.closeSync(fd);
100
+ }
101
+ }
102
+ export function stableWriteJson(filePath, value) {
103
+ stableWrite(filePath, JSON.stringify(value, null, 2) + '\n');
104
+ }
78
105
  /**
79
106
  * 原子读取——自动按状态恢复。文件不存在时返回 null。
80
107
  *
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { compareVersions, resolveGlobalPkg } from './npm-ops.js';
4
4
  import { isWindows as platformIsWindows, resolveCommandPath } from './cross-platform.js';
5
5
  import { MIN_ECWEB_VERSION, WEB_CLI_BIN, WEB_PACKAGE_NAME } from '../product.js';
6
+ import { resolvePaths } from '../paths.js';
6
7
  /** EC Web owns the session-log reader, so older builds cannot safely serve a newer daemon. */
7
8
  export function isEcwebVersionSupported(version) {
8
9
  const normalized = version?.trim();
@@ -70,10 +71,23 @@ export function resolveEcwebLaunchCommand(ecwebArgs, opts = {}) {
70
71
  * 配对码是 ecweb 进程自己生成并持有的内部状态。daemon/CLI 通过 ecweb 的
71
72
  * localhost-only HTTP 接口 GET /api/pair-code 取当前码(远程访问被 ecweb 403 拒绝)。
72
73
  */
73
- /** 经 localhost 拉取 ecweb 当前配对码(仅本机可取)。失败返回 null。 */
74
- export async function fetchEcwebPairCode(port) {
74
+ /** 经 localhost 拉取 ecweb 当前配对码(仅本机可取)。失败返回 null。
75
+ * authorizedBy 仅由 daemon 的 AUN 控制通道传入,用于把配对码换出的 token
76
+ * 绑定到实际批准的 daemon-owner;普通 CLI 取码不携带该字段。 */
77
+ export async function fetchEcwebPairCode(port, authorizedBy) {
75
78
  try {
79
+ const headers = {};
80
+ const owner = authorizedBy?.trim();
81
+ if (owner)
82
+ headers['x-evolcore-pair-authorized-by'] = owner;
83
+ try {
84
+ const controlToken = fs.readFileSync(resolvePaths().controlToken, 'utf8').trim();
85
+ if (controlToken)
86
+ headers['x-evolcore-control-token'] = controlToken;
87
+ }
88
+ catch { }
76
89
  const resp = await fetch(`http://127.0.0.1:${port}/api/pair-code`, {
90
+ headers,
77
91
  signal: AbortSignal.timeout(2000),
78
92
  });
79
93
  if (!resp.ok)
@@ -388,7 +388,10 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
388
388
  const warnPrefix = includeEmoji ? '⚠️ ' : '';
389
389
  const errPrefix = includeEmoji ? '❌ ' : '';
390
390
  if (msg.includes('CONTEXT_COMPACT_FAILED') || isContextTooLongText(msg)) {
391
- return `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
391
+ const detail = msg.match(/^CONTEXT_COMPACT_FAILED:\s*(.+)$/)?.[1]?.trim();
392
+ return detail
393
+ ? `${warnPrefix}上下文过长,自动压缩失败:${detail}`
394
+ : `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
392
395
  }
393
396
  if (isSandboxInitializationFailure(error)) {
394
397
  return `${warnPrefix}只读执行沙箱启动失败(sandbox_initialization_failed),当前任务未执行;请检查 user namespace/seccomp 配置后重试`;
@@ -1,7 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import { resolvePaths } from '../paths.js';
3
3
  import { LogWriter } from './log-writer.js';
4
- import { classifyToolErrorCode } from '../core/permission/tool-error-code.js';
4
+ import { classifyToolErrorCode, normalizeToolErrorCode } from '../core/permission/tool-error-code.js';
5
+ import { normalizePermissionMode } from '../core/permission/mode.js';
5
6
  import { buildToolLifecycleEventKey } from '../core/audit/event-key.js';
6
7
  let currentLevel = process.env.LOG_LEVEL || 'INFO';
7
8
  const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
@@ -70,15 +71,32 @@ export function normalizeStructuredLog(data) {
70
71
  ?? nested?.toolUseId
71
72
  ?? nested?.tool_use_id;
72
73
  const sessionId = data.sessionId ?? data.session_id ?? nested?.sessionId ?? nested?.session_id;
73
- const agentAid = data.agentAid ?? data.agent_aid ?? data.selfAid ?? nested?.agentAid ?? nested?.agent_aid;
74
- const permissionMode = data.permissionMode ?? data.permission_mode ?? nested?.permissionMode ?? nested?.permission_mode;
74
+ const agentAid = data.agentAid ?? data.agent_aid ?? data.selfAid ?? nested?.agentAid ?? nested?.agent_aid ?? nested?.selfAid;
75
+ const rawAgentName = data.agentName ?? data.agent_name ?? nested?.agentName ?? nested?.agent_name;
76
+ // Lifecycle streams should carry both the stable AID and a display label.
77
+ // When a producer only knows the AID, use that same value as the label so
78
+ // daily joins never invent a second identity for one session.
79
+ const normalizedAgentName = typeof rawAgentName === 'string' ? rawAgentName.trim() : '';
80
+ const agentName = normalizedAgentName
81
+ && !['unknown', '<unknown>', '<none>', 'undefined', 'null'].includes(normalizedAgentName)
82
+ ? normalizedAgentName
83
+ : typeof agentAid === 'string' && agentAid !== 'unknown'
84
+ ? agentAid
85
+ : undefined;
86
+ const rawPermissionMode = data.permissionMode ?? data.permission_mode ?? nested?.permissionMode ?? nested?.permission_mode;
87
+ // Normalize legacy values at the log boundary. Preserve an explicit
88
+ // `unknown` marker for records that genuinely arrived without context.
89
+ const permissionMode = typeof rawPermissionMode === 'string'
90
+ && rawPermissionMode !== 'unknown'
91
+ ? normalizePermissionMode(rawPermissionMode).mode
92
+ : rawPermissionMode;
75
93
  const toolName = data.toolName ?? data.tool ?? data.name ?? nested?.toolName ?? nested?.name;
76
94
  const isToolResult = eventType === 'tool:result' || eventType === 'tool_result';
77
95
  const isToolUse = eventType === 'tool:use' || eventType === 'tool_use';
78
96
  const isError = data.isError ?? data.is_error ?? (data.ok === false ? true : undefined)
79
97
  ?? nested?.isError ?? nested?.is_error ?? (nested?.ok === false ? true : undefined);
80
- const error = data.error ?? data.errorMessage ?? nested?.error ?? nested?.errorMessage;
81
- const result = data.result ?? data.content ?? nested?.result ?? nested?.content;
98
+ const decisionSource = data.decisionSource ?? data.decision_source
99
+ ?? nested?.decisionSource ?? nested?.decision_source;
82
100
  const decision = data.decision
83
101
  ?? data.status
84
102
  ?? (isToolResult && isError === true ? 'error' : undefined)
@@ -95,8 +113,11 @@ export function normalizeStructuredLog(data) {
95
113
  ?? nested?.errorCode
96
114
  ?? nested?.error_code;
97
115
  const errorCode = isToolResult && isError === true
98
- ? classifyToolErrorCode({ errorCode: explicitErrorCode, error, result })
116
+ ? classifyToolErrorCode({ errorCode: explicitErrorCode, decisionSource })
99
117
  : explicitErrorCode;
118
+ const classificationMissing = isToolResult && isError === true
119
+ && !normalizeToolErrorCode(explicitErrorCode)
120
+ && decisionSource !== 'policy' && decisionSource !== 'approval';
100
121
  const lifecycleRecord = isToolUse || isToolResult;
101
122
  const lifecycleEventKey = lifecycleRecord
102
123
  ? (eventKey ?? buildToolLifecycleEventKey({
@@ -116,12 +137,15 @@ export function normalizeStructuredLog(data) {
116
137
  ...(correlationId ? { correlationId } : {}),
117
138
  ...(sessionId ? { sessionId } : lifecycleRecord ? { sessionId: 'unknown' } : {}),
118
139
  ...(agentAid ? { agentAid } : lifecycleRecord ? { agentAid: 'unknown' } : {}),
140
+ ...(agentName ? { agentName } : lifecycleRecord ? { agentName: 'unknown' } : {}),
119
141
  ...(permissionMode ? { permissionMode } : lifecycleRecord ? { permissionMode: 'unknown' } : {}),
120
142
  ...(toolName ? { toolName } : {}),
143
+ ...(decisionSource ? { decisionSource } : {}),
121
144
  ...(decision ? { decision } : {}),
122
145
  ...(executed !== undefined ? { executed } : {}),
123
146
  ...(executionState ? { executionState } : {}),
124
147
  ...(errorCode ? { errorCode } : {}),
148
+ ...(classificationMissing ? { classificationMissing: true } : {}),
125
149
  ...(missingContextFields.length > 0 ? { contextMissing: missingContextFields } : {}),
126
150
  };
127
151
  }
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import { spawnSync } from 'child_process';
3
+ import { parseCimDate } from './process-introspect.js';
3
4
  function runnerKind(name) {
4
5
  const normalized = name.toLowerCase();
5
6
  if (normalized.includes('codex'))
@@ -164,7 +165,7 @@ function readUnixPsMetrics() {
164
165
  function readWindowsMetrics() {
165
166
  const command = [
166
167
  'Get-CimInstance Win32_Process',
167
- 'Select-Object ProcessId,ParentProcessId,Name,WorkingSetSize,KernelModeTime,UserModeTime,CreationDate',
168
+ "Select-Object ProcessId,ParentProcessId,Name,WorkingSetSize,KernelModeTime,UserModeTime,@{Name='CreationDate';Expression={if ($_.CreationDate) {$_.CreationDate.ToUniversalTime().ToString('o')}}}",
168
169
  'ConvertTo-Json -Compress',
169
170
  ].join(' | ');
170
171
  const result = spawnSync('powershell', ['-NoProfile', '-Command', command], {
@@ -184,13 +185,32 @@ function readWindowsMetrics() {
184
185
  name: String(row.Name || 'unknown'),
185
186
  rss: Number(row.WorkingSetSize) || 0,
186
187
  cumulativeCpuMs: ((Number(row.KernelModeTime) || 0) + (Number(row.UserModeTime) || 0)) / 10_000,
187
- startId: row.CreationDate ? String(row.CreationDate) : undefined,
188
+ // Keep a comparable numeric start time when PowerShell serializes CIM
189
+ // DateTime as either CIM text or an ISO date. The raw value remains as
190
+ // a fallback so PID reuse protection in ProcessTreeSampler still works
191
+ // when a platform returns an unfamiliar representation.
192
+ startId: parseWindowsCreationDate(row.CreationDate),
188
193
  })).filter((row) => row.pid > 0);
189
194
  }
190
195
  catch {
191
196
  return [];
192
197
  }
193
198
  }
199
+ export function parseWindowsCreationDate(value) {
200
+ if (value === undefined || value === null)
201
+ return undefined;
202
+ const raw = String(value).trim();
203
+ if (!raw)
204
+ return undefined;
205
+ const cim = parseCimDate(raw);
206
+ if (cim !== null)
207
+ return cim;
208
+ const serializedDate = raw.match(/^\/Date\((-?\d+)(?:[+-]\d{4})?\)\/$/);
209
+ if (serializedDate)
210
+ return Number(serializedDate[1]);
211
+ const parsed = Date.parse(raw);
212
+ return Number.isFinite(parsed) ? parsed : raw;
213
+ }
194
214
  export function readProcessMetrics() {
195
215
  if (process.platform === 'linux')
196
216
  return readLinuxMetrics();
@@ -209,9 +229,9 @@ export class ProcessTreeSampler {
209
229
  this.readMetrics = readMetrics;
210
230
  this.now = now;
211
231
  }
212
- sample(associatedRootPids = []) {
232
+ sample(associatedRootPids = [], metricsOverride) {
213
233
  const timestamp = this.now();
214
- const metrics = this.readMetrics();
234
+ const metrics = metricsOverride ?? this.readMetrics();
215
235
  const byPid = new Map(metrics.map(metric => [metric.pid, metric]));
216
236
  const children = new Map();
217
237
  for (const metric of metrics) {
@@ -0,0 +1,31 @@
1
+ import { parentPort } from 'node:worker_threads';
2
+ import { performance } from 'node:perf_hooks';
3
+ import { readProcessMetrics } from './process-tree-stats.js';
4
+ const port = parentPort;
5
+ if (!port)
6
+ throw new Error('process-tree worker requires a parent port');
7
+ port.on('message', (message) => {
8
+ if (message?.type !== 'sample')
9
+ return;
10
+ const startedAt = performance.now();
11
+ try {
12
+ const metrics = readProcessMetrics();
13
+ if (metrics.length === 0)
14
+ throw new Error('process enumeration returned no metrics');
15
+ const response = {
16
+ type: 'sample',
17
+ requestId: message.requestId,
18
+ metrics,
19
+ durationMs: Math.round((performance.now() - startedAt) * 10) / 10,
20
+ };
21
+ port.postMessage(response);
22
+ }
23
+ catch (error) {
24
+ port.postMessage({
25
+ type: 'sample-error',
26
+ requestId: message.requestId,
27
+ error: error instanceof Error ? error.message : String(error),
28
+ durationMs: Math.round((performance.now() - startedAt) * 10) / 10,
29
+ });
30
+ }
31
+ });
@@ -53,8 +53,7 @@ export function defaultProjectsRoot(evolcoreRoot, options = {}) {
53
53
  export function agentProjectRootFromDefaults(defaults, evolcoreRoot, options = {}) {
54
54
  const platform = options.platform ?? process.platform;
55
55
  const pathImpl = pathForPlatform(platform);
56
- const configuredRoot = defaults?.projects?.rootPath
57
- || (defaults?.projects?.defaultPath && pathImpl.dirname(defaults.projects.defaultPath))
56
+ const configuredRoot = defaults?.projects?.defaultPath
58
57
  || defaultProjectsRoot(evolcoreRoot, options);
59
58
  return sanitizeProjectRoot(configuredRoot, options);
60
59
  }
@@ -5,6 +5,65 @@
5
5
  * Edit 工具的摘要为 diff 风格预览(支持 old/new_string 与 unified diff 两种输入)。
6
6
  */
7
7
  import fs from 'fs';
8
+ import { parseLiteralShellArgv, resolveCodexShellCarrierString, } from '../core/permission/ec-command-parser.js';
9
+ const DISPLAY_POSIX_SHELLS = new Set(['bash', 'sh']);
10
+ const DISPLAY_POWERSHELLS = new Set(['pwsh', 'pwsh.exe', 'powershell', 'powershell.exe']);
11
+ const DISPLAY_CMD_SHELLS = new Set(['cmd', 'cmd.exe']);
12
+ const DISPLAY_CMD_INERT_SWITCHES = new Set(['/d', '/s', '/q', '/a', '/u']);
13
+ function executableBasename(value) {
14
+ if (typeof value !== 'string')
15
+ return '';
16
+ return value.replaceAll('\\', '/').split('/').at(-1)?.toLowerCase() ?? '';
17
+ }
18
+ /** Display-only fallback for valid Codex carriers installed outside standard paths. */
19
+ function summarizeDisplayCarrierArgv(argv) {
20
+ if (argv.length === 3) {
21
+ const executable = executableBasename(argv[0]);
22
+ const switchName = argv[1]?.toLowerCase();
23
+ if (DISPLAY_POSIX_SHELLS.has(executable) && switchName === '-lc')
24
+ return argv[2];
25
+ if (DISPLAY_POWERSHELLS.has(executable) && switchName === '-command')
26
+ return argv[2];
27
+ }
28
+ if (argv.length >= 3 && DISPLAY_CMD_SHELLS.has(executableBasename(argv[0]))) {
29
+ const executeIndex = argv.slice(1, -1).findIndex(value => value.toLowerCase() === '/c');
30
+ if (executeIndex >= 0 && executeIndex + 1 === argv.length - 2) {
31
+ const switches = argv.slice(1, executeIndex + 1);
32
+ if (switches.every(value => DISPLAY_CMD_INERT_SWITCHES.has(value.toLowerCase()))) {
33
+ return argv.at(-1);
34
+ }
35
+ }
36
+ }
37
+ return undefined;
38
+ }
39
+ /** Remove Codex app-server's fixed shell carrier from display-only arguments. */
40
+ function displayShellCommand(command) {
41
+ if (typeof command !== 'string' || !command)
42
+ return undefined;
43
+ const parsedArgv = parseLiteralShellArgv(command);
44
+ const display = parsedArgv ? summarizeDisplayCarrierArgv(parsedArgv) : undefined;
45
+ if (display !== undefined)
46
+ return display;
47
+ // The nested PowerShell body is not POSIX shell syntax and can contain
48
+ // quotes, variables, redirects, and statement composition. Use the strict
49
+ // carrier parser only for PowerShell commands that the argv projection
50
+ // cannot parse; ordinary `bash -c` commands must remain unchanged.
51
+ const carrier = resolveCodexShellCarrierString(command);
52
+ return carrier?.dialect === 'powershell' ? carrier.command : undefined;
53
+ }
54
+ /**
55
+ * Build the tool arguments projected into activity/thought payloads.
56
+ * Execution, permission checks, and audit logging keep using the original
57
+ * input; only the human-facing Shell command is de-wrapped here.
58
+ */
59
+ export function toolInputForDisplay(toolName, input) {
60
+ if (!input || toolName !== 'Shell')
61
+ return input;
62
+ const command = displayShellCommand(input.command);
63
+ if (!command || command === input.command)
64
+ return input;
65
+ return { ...input, command };
66
+ }
8
67
  /**
9
68
  * 工具输入摘要(提取工具调用的可读描述,供权限审批和消息展示使用)
10
69
  */
@@ -0,0 +1,201 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ const MAX_CACHE_ENTRIES = 128;
5
+ const signatureCache = new Map();
6
+ /**
7
+ * The signer name is checked only after Authenticode reports a trusted chain.
8
+ * Keep this list deliberately small: an unsigned or unknown Bash distribution
9
+ * must not enter the privileged EC command path.
10
+ */
11
+ const MICROSOFT_PUBLISHER_RE = /(?:^|[,;]\s*)CN=Microsoft (?:Corporation|Windows(?: [^,;]+)?)(?:[,;]|$)/i;
12
+ const MICROSOFT_ORG_RE = /(?:^|[,;]\s*)O=Microsoft Corporation(?:[,;]|$)/i;
13
+ const BASH_PUBLISHER_RES = [
14
+ /(?:^|[,;]\s*)(?:CN|O)=Git for Windows(?:[,;]|$)/i,
15
+ /(?:^|[,;]\s*)(?:CN|O)=The Git Development Community(?:[,;]|$)/i,
16
+ // Git for Windows binaries are currently signed by project maintainer
17
+ // Johannes Schindelin rather than a certificate named after the project.
18
+ /(?:^|[,;]\s*)(?:CN|O)=Johannes Schindelin(?:[,;]|$)/i,
19
+ /(?:^|[,;]\s*)(?:CN|O)=Red Hat,? Inc\.(?:[,;]|$)/i,
20
+ /(?:^|[,;]\s*)(?:CN|O)=MSYS2(?:[,;]|$)/i,
21
+ ];
22
+ function isValidStatus(status) {
23
+ return typeof status === 'string' && status.trim().toLowerCase() === 'valid';
24
+ }
25
+ /** Pure signature policy, separately testable without a Windows host. */
26
+ export function isTrustedShellSignature(kind, evidence) {
27
+ if (!isValidStatus(evidence.status) || typeof evidence.subject !== 'string')
28
+ return false;
29
+ const subject = evidence.subject.trim();
30
+ if (kind === 'cmd' || kind === 'powershell') {
31
+ return MICROSOFT_PUBLISHER_RE.test(subject) && MICROSOFT_ORG_RE.test(subject);
32
+ }
33
+ return (MICROSOFT_PUBLISHER_RE.test(subject) && MICROSOFT_ORG_RE.test(subject))
34
+ || BASH_PUBLISHER_RES.some(pattern => pattern.test(subject));
35
+ }
36
+ function windowsSystemRoot() {
37
+ return process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
38
+ }
39
+ function systemPowerShellPath() {
40
+ return path.win32.join(windowsSystemRoot(), 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
41
+ }
42
+ function systemPowerShellModulePath() {
43
+ return path.win32.join(windowsSystemRoot(), 'System32', 'WindowsPowerShell', 'v1.0', 'Modules');
44
+ }
45
+ /**
46
+ * Windows PowerShell 5.1 must load its own Security module. A caller's
47
+ * PSModulePath can put PowerShell 7's module ahead of it; that module is not
48
+ * compatible with 5.1 and makes Get-AuthenticodeSignature fail to load.
49
+ */
50
+ function systemPowerShellEnvironment() {
51
+ const env = { ...process.env };
52
+ for (const key of Object.keys(env)) {
53
+ if (key.toLowerCase() === 'psmodulepath')
54
+ delete env[key];
55
+ }
56
+ env.PSModulePath = systemPowerShellModulePath();
57
+ return env;
58
+ }
59
+ function systemWherePath() {
60
+ return path.win32.join(windowsSystemRoot(), 'System32', 'where.exe');
61
+ }
62
+ function isAbsoluteWindowsPath(value) {
63
+ return path.win32.isAbsolute(value) || /^\\\\[^\\/]+[\\/][^\\/]+/.test(value);
64
+ }
65
+ function resolveBareShellExecutable(executable, kind) {
66
+ const name = executable.toLowerCase().replace(/\.exe$/i, '');
67
+ if (kind === 'cmd' && name === 'cmd') {
68
+ return path.win32.join(windowsSystemRoot(), 'System32', 'cmd.exe');
69
+ }
70
+ if (kind === 'powershell' && name === 'powershell') {
71
+ return systemPowerShellPath();
72
+ }
73
+ const result = spawnSync(systemWherePath(), [`${name}.exe`], {
74
+ encoding: 'utf8',
75
+ timeout: 3000,
76
+ windowsHide: true,
77
+ stdio: ['ignore', 'pipe', 'ignore'],
78
+ });
79
+ if (result.status !== 0)
80
+ return undefined;
81
+ const first = String(result.stdout || '')
82
+ .split(/\r?\n/)
83
+ .map(line => line.trim())
84
+ .find(Boolean);
85
+ return first || undefined;
86
+ }
87
+ function resolveShellPath(executable, kind) {
88
+ if (!executable)
89
+ return undefined;
90
+ // Codex can keep its POSIX carrier spelling on Windows (for example
91
+ // `/bin/bash`). It is a logical shell name, not a Windows root-relative
92
+ // filesystem path, so resolve it through the trusted Windows PATH lookup.
93
+ if (executable.startsWith('/') && !executable.startsWith('//')) {
94
+ const basename = path.posix.basename(executable);
95
+ return resolveBareShellExecutable(basename, kind);
96
+ }
97
+ if (isAbsoluteWindowsPath(executable))
98
+ return executable;
99
+ return resolveBareShellExecutable(executable, kind);
100
+ }
101
+ function isAppExecutionAliasPath(value) {
102
+ return /[\\/]AppData[\\/]Local[\\/]Microsoft[\\/]WindowsApps[\\/]/i.test(value);
103
+ }
104
+ function shellQuote(value) {
105
+ return `'${value.replaceAll("'", "''")}'`;
106
+ }
107
+ function probeAuthenticode(filePath) {
108
+ const script = [
109
+ '$ErrorActionPreference = "Stop"',
110
+ `$p = ${shellQuote(filePath)}`,
111
+ '$item = Get-Item -LiteralPath $p -Force',
112
+ 'if ($item.PSIsContainer) { exit 17 }',
113
+ '$sig = Get-AuthenticodeSignature -LiteralPath $item.FullName',
114
+ '[pscustomobject]@{',
115
+ ' status = [string]$sig.Status',
116
+ ' subject = if ($sig.SignerCertificate) { [string]$sig.SignerCertificate.Subject } else { "" }',
117
+ ' issuer = if ($sig.SignerCertificate) { [string]$sig.SignerCertificate.Issuer } else { "" }',
118
+ '} | ConvertTo-Json -Compress',
119
+ ].join('\n');
120
+ const result = spawnSync(systemPowerShellPath(), [
121
+ '-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script,
122
+ ], {
123
+ encoding: 'utf8',
124
+ env: systemPowerShellEnvironment(),
125
+ timeout: 5000,
126
+ windowsHide: true,
127
+ stdio: ['ignore', 'pipe', 'ignore'],
128
+ });
129
+ if (result.status !== 0 || result.error || !String(result.stdout || '').trim())
130
+ return undefined;
131
+ try {
132
+ const parsed = JSON.parse(String(result.stdout));
133
+ return {
134
+ status: typeof parsed.status === 'string' ? parsed.status : undefined,
135
+ subject: typeof parsed.subject === 'string' ? parsed.subject : undefined,
136
+ issuer: typeof parsed.issuer === 'string' ? parsed.issuer : undefined,
137
+ };
138
+ }
139
+ catch {
140
+ return undefined;
141
+ }
142
+ }
143
+ function cacheKey(filePath) {
144
+ try {
145
+ return fs.realpathSync.native(filePath).toLowerCase();
146
+ }
147
+ catch {
148
+ return filePath.replaceAll('\\', '/').toLowerCase();
149
+ }
150
+ }
151
+ /**
152
+ * Verify the concrete executable selected by Codex. Non-Windows callers keep
153
+ * the existing parser behavior; only Windows enters the Authenticode path.
154
+ */
155
+ export function isTrustedWindowsShellExecutable(executable, kind) {
156
+ if (process.platform !== 'win32')
157
+ return true;
158
+ const resolved = resolveShellPath(executable, kind);
159
+ if (!resolved)
160
+ return false;
161
+ let finalPath = resolved;
162
+ try {
163
+ finalPath = fs.realpathSync.native(resolved);
164
+ }
165
+ catch {
166
+ // Keep the original path for the signature probe; the stat/probe below
167
+ // will fail closed if the executable is not accessible.
168
+ }
169
+ // If realpath could not escape an App Execution Alias, do not authenticate
170
+ // the alias stub itself. A resolved WindowsApps package binary is allowed
171
+ // and is checked below like any other signed executable.
172
+ if (isAppExecutionAliasPath(finalPath))
173
+ return false;
174
+ let stat;
175
+ try {
176
+ stat = fs.statSync(finalPath);
177
+ if (!stat.isFile())
178
+ return false;
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ const key = cacheKey(finalPath);
184
+ const previous = signatureCache.get(key);
185
+ if (previous && previous.size === stat.size && previous.mtimeMs === stat.mtimeMs) {
186
+ return previous.trusted;
187
+ }
188
+ const trusted = isTrustedShellSignature(kind, probeAuthenticode(finalPath) ?? {});
189
+ signatureCache.set(key, { size: stat.size, mtimeMs: stat.mtimeMs, trusted });
190
+ while (signatureCache.size > MAX_CACHE_ENTRIES) {
191
+ const oldest = signatureCache.keys().next().value;
192
+ if (!oldest)
193
+ break;
194
+ signatureCache.delete(oldest);
195
+ }
196
+ return trusted;
197
+ }
198
+ /** Test hook: clear cached signature decisions after a fixture is replaced. */
199
+ export function clearWindowsShellTrustCache() {
200
+ signatureCache.clear();
201
+ }
@@ -25,7 +25,7 @@
25
25
  | 文档 | 路径 | 说明 |
26
26
  |------|------|------|
27
27
  | 命令集目录 | `evolcore/INDEX.md` | msg/group/agent/aid/storage/ctl/rpc/bench 全集索引 |
28
- | 联系人命令 | `evolcore/contact.md` | Contact Book 拉黑与托管会话权限边界 |
28
+ | 联系人命令 | `evolcore/contact.md` | Contact Book 添加申请、拉黑与托管会话权限边界 |
29
29
 
30
30
  ## 身份
31
31
 
@@ -29,13 +29,13 @@
29
29
  | 命令集 | 用途 | 触发词 | 适用场景 | 文档 |
30
30
  |--------|------|--------|----------|------|
31
31
  | `ec msg` | 私聊收发消息 | 回复/发消息/拉取/撤回/查在线 | 有对端(peerId) | `msg.md` |
32
- | `ec group` | 群聊收发与群管理 | 群发/建群/邀请/踢人/退群/群成员/角色/封禁/规则 | 群聊(groupId) | `group.md` |
32
+ | `ec group` | 群聊收发与群管理 | 群发/建群/邀请/踢人/退群/群成员/角色/封禁/规则/协作 | 群聊(groupId) | `group.md`;完整协作流程见 `group-collaboration.md` |
33
33
  | `ec group rules` | 群规则文件发布与上下文注入 | 群规则/工作流程/职责分工/rules.md/发布规则 | AUN 群聊(groupId) | `group-rules.md` |
34
34
  | `ec aid` | AID 身份管理 | 身份/证书/名片/探测对端 | 任意有渠道场景 | `aid.md` |
35
35
  | `ec fs` | AUN 文件系统统一入口 | 上传/下载/看文件/列目录/删文件/配额/群空间 | 任意有渠道场景 | `fs.md` |
36
36
  | `ec storage` | 文件存储底层调试入口 | storage 调试/旧命令/底层上传下载 | 任意有渠道场景 | `storage.md` |
37
- | `ec agent` | EvolAgent 生命周期 | 创建/启停/热重载/改配置 | 管理员(owner/admin) | `agent.md` |
38
- | `ec contact` | Contact Book 与访问控制 | 拉黑/解除拉黑/查拉黑 | owner/admin;写仅 owner | `contact.md` |
37
+ | `ec agent` | EvolAgent 生命周期 | 创建/启停/热重载/改配置 | 管理员(owner/admin) | `agent.md`;完整创建与 Bootstrap 见 `agent-create.md` |
38
+ | `ec contact` | Contact Book 与访问控制 | 申请添加、拉黑/解除拉黑、查拉黑 | Agent 当前私聊可申请添加;管理写操作按角色限制 | `contact.md` |
39
39
  | `ec rpc` | 底层 AUN RPC(逃生通道) | 直接调协议方法 | 高级/兜底 | `rpc.md` |
40
40
 
41
41
  ## 会话自管理类(不连 AUN,操作当前会话)