evolcore 0.0.18 → 0.0.19

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## 0.0.19 (2026-08-27)
7
+
8
+ ### PowerShell EC 命令安全
9
+
10
+ - 支持 PowerShell `pwsh`/`powershell` 受管命令载体解析,拒绝拼接、展开和重定向。
11
+ - 统一 Codex EC 命令委派与预检解析,补充 PowerShell 引号、展开和错误指引。
12
+
13
+ ### Bootstrap 与运行时控制
14
+
15
+ - Bootstrap 阶段保留 Menu 与斜杠协议处理,普通模型任务继续按生命周期门禁。
16
+ - 命令意外泄漏至响应引擎时统一丢弃并记录失败审计,避免进入 Agent 执行。
17
+
18
+ ### 跨平台安装与诊断
19
+
20
+ - Windows 数据迁移固定调用系统 `bsdtar`,避免 Git Bash GNU tar 误解析 Windows 绝对路径。
21
+ - Linux 安装器幂等创建真实 `/etc/codex` 目录,拒绝同名文件和符号链接。
22
+ - 统一数据根解析,仅使用 `EVOLCORE_HOME` 或 `~/.evolcore`,避免因启动目录不同误用数据目录。
23
+ - `ec start` 已运行提示新增 Daemon 持续运行时长,并支持 IPC uptime 回退探测。
24
+
6
25
  ## 0.0.18 (2026-08-27)
7
26
 
8
27
  ### DaemonOwner 与受管 Agent 操作
@@ -20,7 +20,7 @@ import { buildEnvelope, sendInteractionPayload } from '../core/message/message-u
20
20
  import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
21
21
  import { AGENT_DELEGATION_TOKEN_ENV, hashDelegatedCommandArgv } from '../core/auth/agent-delegation.js';
22
22
  import { sanitizeShellExecutionEnvironment } from '../core/permission/shell-environment.js';
23
- import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, parseLiteralShellArgv, unwrapCodexShellCommandArgv, } from '../core/permission/ec-command-parser.js';
23
+ import { classifyEvolcoreShellCommand, containsLiteralManagedTmpDirOutsideSendContent, parseLiteralShellArgv, resolveCodexShellCarrierArgv, } from '../core/permission/ec-command-parser.js';
24
24
  import { compareVersions } from '../utils/npm-ops.js';
25
25
  import { resolvePaths, resolveRoot } from '../paths.js';
26
26
  import { ensureProcessManagedTempDir } from '../cli/task-context.js';
@@ -316,18 +316,20 @@ export class CodexRunner {
316
316
  createDelegationCarrier() {
317
317
  return randomBytes(32).toString('base64url');
318
318
  }
319
- approvedLiteralEvolcoreCommandArgv(command, managedTempDir) {
320
- const classification = classifyEvolcoreShellCommand(command);
319
+ approvedLiteralEvolcoreCommandArgv(command, managedTempDir, dialect = 'posix') {
320
+ const parseOptions = {
321
+ allowManagedTmpDir: true,
322
+ dialect,
323
+ ...(managedTempDir ? { managedTempDir } : {}),
324
+ };
325
+ const classification = classifyEvolcoreShellCommand(command, parseOptions);
321
326
  if (classification.kind === 'bounded-output')
322
327
  return classification.command.argv;
323
328
  if (classification.kind !== 'literal')
324
329
  return undefined;
325
330
  // Keep the approval/delegation parser aligned with the preflight parser:
326
331
  // the session-managed `$TMPDIR` token is a permitted EC path reference.
327
- const argv = parseLiteralShellArgv(command, {
328
- allowManagedTmpDir: true,
329
- ...(managedTempDir ? { managedTempDir } : {}),
330
- });
332
+ const argv = parseLiteralShellArgv(command, parseOptions);
331
333
  if (argv && containsLiteralManagedTmpDirOutsideSendContent(argv))
332
334
  return undefined;
333
335
  return argv?.[0] === 'ec' ? argv : undefined;
@@ -343,11 +345,11 @@ export class CodexRunner {
343
345
  ? undefined
344
346
  : explicitArgv;
345
347
  }
346
- const explicitWrappedCommand = explicitArgv
347
- ? unwrapCodexShellCommandArgv(explicitArgv)
348
+ const explicitCarrier = explicitArgv
349
+ ? resolveCodexShellCarrierArgv(explicitArgv)
348
350
  : undefined;
349
- if (explicitWrappedCommand !== undefined) {
350
- return this.approvedLiteralEvolcoreCommandArgv(explicitWrappedCommand, managedTempDir);
351
+ if (explicitCarrier !== undefined) {
352
+ return this.approvedLiteralEvolcoreCommandArgv(explicitCarrier.command, managedTempDir, explicitCarrier.dialect);
351
353
  }
352
354
  const command = typeof toolInput.command === 'string' ? toolInput.command : '';
353
355
  const directArgv = this.approvedLiteralEvolcoreCommandArgv(command, managedTempDir);
@@ -357,12 +359,12 @@ export class CodexRunner {
357
359
  allowManagedTmpDir: true,
358
360
  ...(managedTempDir ? { managedTempDir } : {}),
359
361
  });
360
- const wrappedCommand = wrappedArgv
361
- ? unwrapCodexShellCommandArgv(wrappedArgv)
362
+ const shellCarrier = wrappedArgv
363
+ ? resolveCodexShellCarrierArgv(wrappedArgv)
362
364
  : undefined;
363
- return wrappedCommand === undefined
365
+ return shellCarrier === undefined
364
366
  ? undefined
365
- : this.approvedLiteralEvolcoreCommandArgv(wrappedCommand, managedTempDir);
367
+ : this.approvedLiteralEvolcoreCommandArgv(shellCarrier.command, managedTempDir, shellCarrier.dialect);
366
368
  }
367
369
  isManagedEvolcoreCommandIntent(toolInput) {
368
370
  if (Array.isArray(toolInput.commandArgv)) {
@@ -371,9 +373,11 @@ export class CodexRunner {
371
373
  return true;
372
374
  if (!rawArgv.every(value => typeof value === 'string'))
373
375
  return false;
374
- const wrappedCommand = unwrapCodexShellCommandArgv(rawArgv);
375
- return wrappedCommand !== undefined
376
- && classifyEvolcoreShellCommand(wrappedCommand).kind !== 'none';
376
+ const shellCarrier = resolveCodexShellCarrierArgv(rawArgv);
377
+ return shellCarrier !== undefined
378
+ && classifyEvolcoreShellCommand(shellCarrier.command, {
379
+ dialect: shellCarrier.dialect,
380
+ }).kind !== 'none';
377
381
  }
378
382
  const command = typeof toolInput.command === 'string' ? toolInput.command : '';
379
383
  return classifyEvolcoreShellCommand(command).kind !== 'none';
@@ -218,6 +218,33 @@ function formatLocalTime(ms) {
218
218
  const d = new Date(ms);
219
219
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
220
220
  }
221
+ function formatRunningDuration(ms) {
222
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
223
+ const seconds = totalSeconds % 60;
224
+ const totalMinutes = Math.floor(totalSeconds / 60);
225
+ if (totalMinutes === 0)
226
+ return `${seconds}秒`;
227
+ const minutes = totalMinutes % 60;
228
+ const totalHours = Math.floor(totalMinutes / 60);
229
+ if (totalHours === 0)
230
+ return `${minutes}分${seconds}秒`;
231
+ const hours = totalHours % 24;
232
+ const days = Math.floor(totalHours / 24);
233
+ if (days === 0)
234
+ return `${hours}小时${minutes}分${seconds}秒`;
235
+ return `${days}天${hours}小时${minutes}分${seconds}秒`;
236
+ }
237
+ function resolveRunningDuration(pid, uptime) {
238
+ if (typeof uptime === 'number' && Number.isFinite(uptime) && uptime >= 0) {
239
+ return formatRunningDuration(uptime);
240
+ }
241
+ const processStartedAt = getProcessStartTime(pid);
242
+ if (processStartedAt !== null) {
243
+ return formatRunningDuration(Date.now() - processStartedAt);
244
+ }
245
+ const instance = scanInstances().mains.find(entry => entry.alive && entry.record.pid === pid);
246
+ return instance ? formatRunningDuration(Date.now() - instance.record.startedAt) : '未知';
247
+ }
221
248
  function printStartupInfo(opts = {}) {
222
249
  const pkgRoot = getPackageRoot();
223
250
  const isNpmInstall = pkgRoot.includes('node_modules');
@@ -273,7 +300,7 @@ export async function cmdStart(opts = {}) {
273
300
  const existingDaemon = await probeDaemon(p.socket);
274
301
  if (existingDaemon) {
275
302
  console.log(` EvolCore is already running (PID: ${existingDaemon.pid}, IPC reachable)`);
276
- console.log(' 已在运行;部署/脚本可直接复用当前实例');
303
+ console.log(` 已在运行;已运行时长: ${resolveRunningDuration(existingDaemon.pid, existingDaemon.uptime)};部署/脚本可直接复用当前实例`);
277
304
  return;
278
305
  }
279
306
  ensureDataDirs();
@@ -373,7 +400,7 @@ export async function cmdStart(opts = {}) {
373
400
  console.log(` ${symbol} ${aid} — 最后活动 ${ago} (${info.event})`);
374
401
  }
375
402
  }
376
- console.log(' 已在运行;部署/脚本可继续轮询 IPC ready,或直接复用当前实例');
403
+ console.log(` 已在运行;已运行时长: ${formatRunningDuration(Date.now() - first.record.startedAt)};部署/脚本可继续轮询 IPC ready,或直接复用当前实例`);
377
404
  console.log(' 使用 ec restart 重启,或 ec stop 先停止');
378
405
  return;
379
406
  }
@@ -1295,8 +1295,14 @@ function finalizationSourcePaths(root, manifest) {
1295
1295
  function archiveHash(filePath) {
1296
1296
  return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
1297
1297
  }
1298
- function verifyArchiveContains(archivePath, sourcePaths) {
1299
- const tar = process.platform === 'win32' ? 'tar.exe' : 'tar';
1298
+ function systemTar() {
1299
+ if (process.platform !== 'win32')
1300
+ return 'tar';
1301
+ // Git Bash puts GNU tar ahead of Windows bsdtar on PATH. GNU tar treats the
1302
+ // drive colon in an absolute archive path as host:path remote syntax.
1303
+ return path.win32.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe');
1304
+ }
1305
+ function verifyArchiveContains(archivePath, sourcePaths, tar) {
1300
1306
  const normalizeArchivePath = (value) => value
1301
1307
  .trim()
1302
1308
  .replace(/\\/g, '/')
@@ -1402,14 +1408,14 @@ export function finalizeDataMigration(root, manifest) {
1402
1408
  }
1403
1409
  fs.mkdirSync(path.dirname(archivePath), { recursive: true });
1404
1410
  const temporary = `${archivePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
1405
- const tar = process.platform === 'win32' ? 'tar.exe' : 'tar';
1411
+ const tar = systemTar();
1406
1412
  try {
1407
1413
  execFileSync(tar, ['-czf', temporary, ...sourcePaths], {
1408
1414
  cwd: root,
1409
1415
  stdio: 'pipe',
1410
1416
  windowsHide: process.platform === 'win32',
1411
1417
  });
1412
- verifyArchiveContains(temporary, sourcePaths);
1418
+ verifyArchiveContains(temporary, sourcePaths, tar);
1413
1419
  fs.renameSync(temporary, archivePath);
1414
1420
  }
1415
1421
  catch (error) {
@@ -695,12 +695,6 @@ export class MessageBridge {
695
695
  const channelKey = adapter?.channelKey || channelName;
696
696
  const owningAgent = this.agentRegistry?.resolveByChannel(channelKey)
697
697
  ?? this.agentRegistry?.resolveByChannel(channelName);
698
- // Agent-owned business controls bypass ResponseEngine entirely, so
699
- // expose them only after bootstrap has explicitly reached active.
700
- // Missing lifecycle fails closed; control channels without an owning
701
- // EvolAgent keep their existing command behavior.
702
- const allowsNormalCommandRouting = !owningAgent
703
- || owningAgent.config?.lifecycle === 'active';
704
698
  const parsedChannelKey = tryParseChannelKey(channelKey);
705
699
  const chatType = msg.chatType || 'private';
706
700
  const delivery = chatType === 'group'
@@ -779,7 +773,10 @@ export class MessageBridge {
779
773
  logger.info(`[MessageBridge] Inbound admission denied before business routing: self=${selfAid} actor=${actorId ?? '<none>'} channel=${channelKey} reason=${admissionDecision.reason}`);
780
774
  return;
781
775
  }
782
- const menuControl = allowsNormalCommandRouting && resolvedChannelType === 'aun'
776
+ // Menu is a protocol control plane. Its parsing and response path must
777
+ // remain available throughout the agent lifecycle; lifecycle gates
778
+ // ordinary model execution, not protocol handling.
779
+ const menuControl = resolvedChannelType === 'aun'
783
780
  ? parseMenuControl(content)
784
781
  : { isMenu: false };
785
782
  let menuResponseWarning;
@@ -930,8 +927,7 @@ export class MessageBridge {
930
927
  // 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
931
928
  const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
932
929
  const cmdContent = contentForCmd || content;
933
- const isCmd = allowsNormalCommandRouting
934
- && msg.source !== 'handoff'
930
+ const isCmd = msg.source !== 'handoff'
935
931
  && this.cmdHandler.isCommand(cmdContent);
936
932
  if (isCmd) {
937
933
  logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
@@ -969,8 +965,7 @@ export class MessageBridge {
969
965
  logger.debug(`[MessageBridge] Failed to log inbound command: ${e}`);
970
966
  }
971
967
  }
972
- if (allowsNormalCommandRouting
973
- && msg.source !== 'handoff'
968
+ if (msg.source !== 'handoff'
974
969
  && await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
975
970
  const taskId = `cmd-${msg.messageId || Date.now()}`;
976
971
  logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId, correlationId: taskId, sessionId: msg.replyContext?.sessionId, agentAid: msg.selfAID, payload: { kind: 'command.result', text } });
@@ -48,7 +48,58 @@ import { SessionTurnCoordinator } from '../session/session-turn-coordinator.js';
48
48
  import { recordTriggerExecutionAnomaly } from '../../trigger/anomaly-store.js';
49
49
  import { classifyToolErrorCode } from '../permission/tool-error-code.js';
50
50
  import { buildToolLifecycleEventKey } from '../audit/event-key.js';
51
- import { PauseController } from './pause-controller.js';
51
+ export class PauseController {
52
+ state = 'running';
53
+ waiters = new Set();
54
+ requestPause() {
55
+ if (this.state !== 'running')
56
+ return false;
57
+ this.state = 'pause_requested';
58
+ return true;
59
+ }
60
+ isPauseRequested() { return this.state === 'pause_requested'; }
61
+ isPaused() { return this.state === 'paused'; }
62
+ waitAtToolBoundary(signal) {
63
+ if (signal?.aborted)
64
+ return Promise.resolve('cancelled');
65
+ if (this.state === 'running')
66
+ return Promise.resolve('none');
67
+ this.state = 'paused';
68
+ return new Promise(resolve => {
69
+ const waiter = { resolve: () => { }, abort: undefined };
70
+ const finish = (result) => {
71
+ this.waiters.delete(waiter);
72
+ if (waiter.abort)
73
+ signal?.removeEventListener('abort', waiter.abort);
74
+ resolve(result);
75
+ };
76
+ waiter.resolve = finish;
77
+ if (signal) {
78
+ waiter.abort = () => finish('cancelled');
79
+ signal.addEventListener('abort', waiter.abort, { once: true });
80
+ }
81
+ this.waiters.add(waiter);
82
+ if (this.state === 'running')
83
+ finish('released');
84
+ else if (signal?.aborted)
85
+ finish('cancelled');
86
+ });
87
+ }
88
+ resume() {
89
+ if (this.state === 'running')
90
+ return false;
91
+ this.state = 'running';
92
+ for (const waiter of [...this.waiters])
93
+ waiter.resolve('released');
94
+ return true;
95
+ }
96
+ cancel() {
97
+ this.state = 'running';
98
+ for (const waiter of [...this.waiters])
99
+ waiter.resolve('cancelled');
100
+ }
101
+ clear() { this.cancel(); }
102
+ }
52
103
  function isShowActivitiesMode(value) {
53
104
  return value === 'all' || value === 'text' || value === 'none';
54
105
  }
@@ -1577,7 +1628,7 @@ export class ResponseEngine {
1577
1628
  // 二次拦截:如果命令消息绕过 MessageBridge 的 handleCommand 泄漏到这里,
1578
1629
  // 静默丢弃而不是发送给 Agent(命令已在 MessageBridge 层处理过)
1579
1630
  const rawContent = message.content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
1580
- if (!isBootstrapping && rawContent.startsWith('/') && this.isKnownCommand(rawContent)) {
1631
+ if (rawContent.startsWith('/') && this.isKnownCommand(rawContent)) {
1581
1632
  logger.warn(`[ResponseEngine] Command leaked past MessageBridge, dropped: "${rawContent.substring(0, 40)}"`);
1582
1633
  this.publishTriggerExecutionFailure(message, 'trigger_command_not_supported');
1583
1634
  return;
@@ -1,14 +1,21 @@
1
1
  const HEAD_EXECUTABLES = new Set(['head', '/bin/head', '/usr/bin/head']);
2
2
  // Codex app-server sends command executions through a fixed shell carrier.
3
3
  // Treat the carrier as syntax only after parsing it as literal argv; never
4
- // accept an arbitrary `sh -c` string or unwrap shell composition here.
5
- const CODEX_SHELL_EXECUTABLES = new Set([
4
+ // accept an arbitrary shell command string or unwrap composition here.
5
+ const CODEX_POSIX_SHELL_EXECUTABLES = new Set([
6
6
  'bash', '/bin/bash', '/usr/bin/bash',
7
7
  'sh', '/bin/sh', '/usr/bin/sh',
8
8
  ]);
9
+ const CODEX_POWERSHELL_EXECUTABLES = new Set([
10
+ 'pwsh', 'pwsh.exe', 'powershell', 'powershell.exe',
11
+ ]);
12
+ const CODEX_POWERSHELL_INSTALL_PATH_RE = /^[A-Za-z]:[\\/](?:Program Files[\\/]PowerShell[\\/][0-9]+(?:\.[0-9]+)*(?:-[A-Za-z0-9.-]+)?[\\/]pwsh\.exe|Windows[\\/]System32[\\/]WindowsPowerShell[\\/]v1\.0[\\/]powershell\.exe)$/i;
9
13
  const SHELL_UNQUOTED_META = new Set([
10
14
  ';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
11
15
  ]);
16
+ const POWERSHELL_UNQUOTED_META = new Set([
17
+ ';', '&', '|', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#', '@',
18
+ ]);
12
19
  const AMBIGUOUS_FILE_PATH_RE = /[\0\r\n$`*?\[\]{}<>|;&]/;
13
20
  const EVOLCORE_COMMAND_POSITION_RE = /(?:^|[;&|()`])\s*ec(?=\s|[;&|()<>`]|$)/i;
14
21
  function trimShellSpacing(value) {
@@ -83,6 +90,106 @@ function isLiteralArgBoundary(value) {
83
90
  function isShellParameterExpansionStart(value) {
84
91
  return value === '(' || value === '{' || !!value && /[A-Za-z0-9_@*#?$!\-]/.test(value);
85
92
  }
93
+ function parseLiteralPowerShellCommand(command) {
94
+ const input = trimShellSpacing(command);
95
+ if (!input)
96
+ return { ok: false, issue: 'empty-command' };
97
+ if (input.includes('\0'))
98
+ return { ok: false, issue: 'invalid-control-char' };
99
+ const argv = [];
100
+ let token = '';
101
+ let tokenStarted = false;
102
+ let quote = null;
103
+ const finishToken = () => {
104
+ if (!tokenStarted)
105
+ return;
106
+ argv.push(token);
107
+ token = '';
108
+ tokenStarted = false;
109
+ };
110
+ for (let index = 0; index < input.length; index++) {
111
+ const char = input[index];
112
+ if (quote === 'single') {
113
+ if (char === "'" && input[index + 1] === "'") {
114
+ token += "'";
115
+ index++;
116
+ }
117
+ else if (char === "'") {
118
+ if (!isLiteralArgBoundary(input[index + 1]))
119
+ return { ok: false, issue: 'invalid-quote' };
120
+ quote = null;
121
+ }
122
+ else {
123
+ token += char;
124
+ }
125
+ continue;
126
+ }
127
+ if (quote === 'double') {
128
+ if (char === '"') {
129
+ if (!isLiteralArgBoundary(input[index + 1]))
130
+ return { ok: false, issue: 'invalid-quote' };
131
+ quote = null;
132
+ continue;
133
+ }
134
+ if (char === '`') {
135
+ const next = input[++index];
136
+ if (next === undefined)
137
+ return { ok: false, issue: 'invalid-quote' };
138
+ if (next === '\r' || next === '\n')
139
+ return { ok: false, issue: 'structural-newline' };
140
+ token += next;
141
+ continue;
142
+ }
143
+ if (char === '$' && isShellParameterExpansionStart(input[index + 1])) {
144
+ return { ok: false, issue: 'unsafe-expansion' };
145
+ }
146
+ token += char;
147
+ continue;
148
+ }
149
+ if (char === ' ' || char === '\t') {
150
+ finishToken();
151
+ continue;
152
+ }
153
+ if (char === '\r' || char === '\n')
154
+ return { ok: false, issue: 'structural-newline' };
155
+ if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
156
+ return { ok: false, issue: 'invalid-control-char' };
157
+ }
158
+ if (char === "'") {
159
+ tokenStarted = true;
160
+ quote = 'single';
161
+ continue;
162
+ }
163
+ if (char === '"') {
164
+ tokenStarted = true;
165
+ quote = 'double';
166
+ continue;
167
+ }
168
+ if (char === '`') {
169
+ const next = input[++index];
170
+ if (next === undefined)
171
+ return { ok: false, issue: 'invalid-quote' };
172
+ if (next === '\r' || next === '\n')
173
+ return { ok: false, issue: 'structural-newline' };
174
+ tokenStarted = true;
175
+ token += next;
176
+ continue;
177
+ }
178
+ const isWordInitialMeta = (char === '#' || char === '@') && !tokenStarted;
179
+ const isAlwaysMeta = POWERSHELL_UNQUOTED_META.has(char) && char !== '#' && char !== '@';
180
+ if (isAlwaysMeta || isWordInitialMeta || (char === '~' && !tokenStarted)) {
181
+ return { ok: false, issue: 'shell-composition' };
182
+ }
183
+ tokenStarted = true;
184
+ token += char;
185
+ }
186
+ if (quote)
187
+ return { ok: false, issue: 'invalid-quote' };
188
+ finishToken();
189
+ return argv.length > 0
190
+ ? { ok: true, argv }
191
+ : { ok: false, issue: 'empty-command' };
192
+ }
86
193
  function doubleQuotedIssueAt(input, index) {
87
194
  const char = input[index];
88
195
  if (char === '`' || (char === '$' && isShellParameterExpansionStart(input[index + 1]))) {
@@ -102,6 +209,9 @@ function doubleQuotedIssueAt(input, index) {
102
209
  * Anything outside this subset is never granted the privileged EC path.
103
210
  */
104
211
  export function parseLiteralShellCommand(command, options = {}) {
212
+ if (options.dialect === 'powershell') {
213
+ return parseLiteralPowerShellCommand(command);
214
+ }
105
215
  const input = trimShellSpacing(options.allowManagedTmpDir
106
216
  ? substituteManagedTmpDirToken(command)
107
217
  : command);
@@ -209,15 +319,21 @@ export function parseLiteralShellArgv(command, options = {}) {
209
319
  const result = parseLiteralShellCommand(command, options);
210
320
  return result.ok ? result.argv : null;
211
321
  }
212
- /**
213
- * Return the command string carried by Codex's literal `/bin/bash -lc ...`
214
- * wrapper. The wrapper is accepted only in its exact three-argv form and the
215
- * caller must parse the returned string with the literal shell parser again.
216
- */
217
- export function unwrapCodexShellCommandArgv(argv) {
218
- if (argv.length !== 3 || argv[1] !== '-lc')
322
+ export function resolveCodexShellCarrierArgv(argv) {
323
+ if (argv.length !== 3)
219
324
  return undefined;
220
- return CODEX_SHELL_EXECUTABLES.has(argv[0]) ? argv[2] : undefined;
325
+ if (argv[1] === '-lc' && CODEX_POSIX_SHELL_EXECUTABLES.has(argv[0])) {
326
+ return { command: argv[2], dialect: 'posix' };
327
+ }
328
+ const powershellExecutable = argv[0].toLowerCase();
329
+ const isPowerShell = CODEX_POWERSHELL_EXECUTABLES.has(powershellExecutable)
330
+ || CODEX_POWERSHELL_INSTALL_PATH_RE.test(argv[0]);
331
+ return isPowerShell && argv[1].toLowerCase() === '-command'
332
+ ? { command: argv[2], dialect: 'powershell' }
333
+ : undefined;
334
+ }
335
+ export function unwrapCodexShellCommandArgv(argv) {
336
+ return resolveCodexShellCarrierArgv(argv)?.command;
221
337
  }
222
338
  /**
223
339
  * Parse either a bare literal command or the exact shell carrier emitted by
@@ -228,8 +344,10 @@ export function parseCodexShellCommandArgv(command, options = {}) {
228
344
  const outer = parseLiteralShellArgv(command, options);
229
345
  if (!outer)
230
346
  return null;
231
- const wrapped = unwrapCodexShellCommandArgv(outer);
232
- return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped, options);
347
+ const carrier = resolveCodexShellCarrierArgv(outer);
348
+ return carrier === undefined
349
+ ? outer
350
+ : parseLiteralShellArgv(carrier.command, { ...options, dialect: carrier.dialect });
233
351
  }
234
352
  /**
235
353
  * Resolve a Codex command input while preserving the raw `commandArgv` form
@@ -239,10 +357,13 @@ export function parseCodexShellCommandArgv(command, options = {}) {
239
357
  export function parseCodexToolCommandArgv(input) {
240
358
  if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
241
359
  const outer = input.commandArgv;
242
- const wrapped = unwrapCodexShellCommandArgv(outer);
243
- return wrapped === undefined
360
+ const carrier = resolveCodexShellCarrierArgv(outer);
361
+ return carrier === undefined
244
362
  ? outer
245
- : parseLiteralShellArgv(wrapped, { allowManagedTmpDir: true });
363
+ : parseLiteralShellArgv(carrier.command, {
364
+ allowManagedTmpDir: true,
365
+ dialect: carrier.dialect,
366
+ });
246
367
  }
247
368
  const command = typeof input.command === 'string' ? input.command : '';
248
369
  return parseCodexShellCommandArgv(command, { allowManagedTmpDir: true });
@@ -924,24 +1045,29 @@ export function parseBoundedOutputShellCommand(command) {
924
1045
  * policy must not interpret EC subcommands or arguments: a single literal
925
1046
  * invocation may leave the sandbox, while shell composition may not.
926
1047
  */
927
- export function classifyEvolcoreShellCommand(command) {
928
- const parseOptions = { allowManagedTmpDir: true };
1048
+ export function classifyEvolcoreShellCommand(command, options = {}) {
1049
+ const parseOptions = { allowManagedTmpDir: true, ...options };
929
1050
  const literal = parseLiteralShellCommand(command, parseOptions);
930
1051
  if (literal.ok) {
931
1052
  if (literal.argv[0] === 'ec')
932
1053
  return { kind: 'literal' };
933
- const wrapped = unwrapCodexShellCommandArgv(literal.argv);
934
- if (wrapped !== undefined) {
935
- const nested = parseLiteralShellCommand(wrapped, parseOptions);
1054
+ const carrier = resolveCodexShellCarrierArgv(literal.argv);
1055
+ if (carrier !== undefined) {
1056
+ const nested = parseLiteralShellCommand(carrier.command, {
1057
+ ...parseOptions,
1058
+ dialect: carrier.dialect,
1059
+ });
936
1060
  if (nested.ok && nested.argv[0] === 'ec')
937
1061
  return { kind: 'literal' };
938
- if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(wrapped)) {
1062
+ if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(carrier.command)) {
939
1063
  return { kind: 'composite', issue: nested.issue };
940
1064
  }
941
1065
  }
942
1066
  return { kind: 'none' };
943
1067
  }
944
- const boundedOutput = parseBoundedOutputShellCommand(command);
1068
+ const boundedOutput = options.dialect === 'powershell'
1069
+ ? null
1070
+ : parseBoundedOutputShellCommand(command);
945
1071
  if (boundedOutput && boundedOutput.argv[0] === 'ec') {
946
1072
  return { kind: 'bounded-output', command: boundedOutput };
947
1073
  }
@@ -4,7 +4,7 @@ import { randomUUID } from 'crypto';
4
4
  import { logger } from '../../utils/logger.js';
5
5
  import { resolveRoot } from '../../paths.js';
6
6
  import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from '../protected-paths.js';
7
- import { classifyEvolcoreShellCommand, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv, unwrapCodexShellCommandArgv, } from './ec-command-parser.js';
7
+ import { classifyEvolcoreShellCommand, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, } from './ec-command-parser.js';
8
8
  import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
9
9
  /** Resolve the session-owned temporary root supplied by the execution host. */
10
10
  export function resolveManagedTempDir(env = process.env) {
@@ -1352,7 +1352,8 @@ export function evaluateToolPreflight(toolName, input, context) {
1352
1352
  ? input.commandArgv
1353
1353
  : undefined;
1354
1354
  const outerArgv = explicitCommandArgv ?? parseLiteralShellArgv(command) ?? undefined;
1355
- const wrappedCommand = outerArgv ? unwrapCodexShellCommandArgv(outerArgv) : undefined;
1355
+ const shellCarrier = outerArgv ? resolveCodexShellCarrierArgv(outerArgv) : undefined;
1356
+ const wrappedCommand = shellCarrier?.command;
1356
1357
  const policyCommand = wrappedCommand ?? command;
1357
1358
  if (context.sessionId && /^\s*(?:command\s+-v|which|type)\s+ec\s*$/i.test(policyCommand)) {
1358
1359
  return {
@@ -1397,7 +1398,9 @@ export function evaluateToolPreflight(toolName, input, context) {
1397
1398
  policyCode: 'managed_ec_aid_scope_forbidden',
1398
1399
  };
1399
1400
  }
1400
- const ecCommand = classifyEvolcoreShellCommand(policyCommand);
1401
+ const ecCommand = classifyEvolcoreShellCommand(policyCommand, shellCarrier
1402
+ ? { dialect: shellCarrier.dialect }
1403
+ : {});
1401
1404
  if (ecCommand.kind === 'literal') {
1402
1405
  return { behavior: 'allow', input, reason: 'ec-command' };
1403
1406
  }
@@ -1406,25 +1409,34 @@ export function evaluateToolPreflight(toolName, input, context) {
1406
1409
  }
1407
1410
  if (ecCommand.kind === 'composite') {
1408
1411
  if (ecCommand.issue === 'unsafe-expansion') {
1412
+ const message = shellCarrier?.dialect === 'powershell'
1413
+ ? '🔒 EC 双引号正文包含未转义的 PowerShell 展开;纯文字请优先使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。PowerShell 传给 ec 时会还原为原文字面量'
1414
+ : '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量';
1409
1415
  return {
1410
1416
  behavior: 'deny',
1411
1417
  input,
1412
- message: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量',
1418
+ message,
1413
1419
  policyCode: 'ec_shell_unsafe_expansion',
1414
1420
  };
1415
1421
  }
1416
1422
  if (ecCommand.issue === 'invalid-quote') {
1423
+ const message = shellCarrier?.dialect === 'powershell'
1424
+ ? '🔒 EC 命令引号未闭合或存在错误嵌套;PowerShell 正文内的双引号请用反引号转义,或改用单引号包裹纯文字'
1425
+ : '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"';
1417
1426
  return {
1418
1427
  behavior: 'deny',
1419
1428
  input,
1420
- message: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"',
1429
+ message,
1421
1430
  policyCode: 'ec_shell_invalid_quote',
1422
1431
  };
1423
1432
  }
1433
+ const message = shellCarrier?.dialect === 'powershell'
1434
+ ? '🔒 EC 命令调用被拒绝:一次 PowerShell/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要拼接探测命令,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开或子表达式追加其它命令。'
1435
+ : '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。';
1424
1436
  return {
1425
1437
  behavior: 'deny',
1426
1438
  input,
1427
- message: '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。',
1439
+ message,
1428
1440
  policyCode: 'ec_shell_composite_command',
1429
1441
  };
1430
1442
  }
package/dist/ipc.js CHANGED
@@ -435,7 +435,12 @@ export class IpcServer {
435
435
  }
436
436
  }
437
437
  case 'ping':
438
- return { pong: true, pid: process.pid, protocolVersion: 1 };
438
+ return {
439
+ pong: true,
440
+ pid: process.pid,
441
+ protocolVersion: 1,
442
+ uptime: Math.round(process.uptime() * 1000),
443
+ };
439
444
  case 'shutdown': {
440
445
  if (!this.shutdownExecutor)
441
446
  return { ok: false, error: 'shutdown executor not configured' };
package/dist/paths.js CHANGED
@@ -11,9 +11,6 @@ export function resolveRoot() {
11
11
  if (process.env[RUNTIME_HOME_ENV]) {
12
12
  _root = process.env[RUNTIME_HOME_ENV];
13
13
  }
14
- else if (fs.existsSync(path.join(process.cwd(), 'agents', 'defaults.json'))) {
15
- _root = process.cwd();
16
- }
17
14
  else {
18
15
  _root = path.join(os.homedir(), DEFAULT_RUNTIME_HOME_DIRNAME);
19
16
  }
@@ -7,7 +7,7 @@
7
7
 
8
8
  | 名称 | 含义 | 来源 |
9
9
  |------|------|------|
10
- | `$EVOLCORE_HOME` | evolcore 用户数据根 | `resolveRoot()`:环境变量 `EVOLCORE_HOME` → cwd 下有 `agents/defaults.json` 则取 cwd → `~/.evolcore` |
10
+ | `$EVOLCORE_HOME` | evolcore 用户数据根 | `resolveRoot()`:环境变量 `EVOLCORE_HOME` → `~/.evolcore` |
11
11
  | `$PACKAGE_ROOT` | evolcore 包根目录 | `getPackageRoot()`,由 `import.meta` 推出(不是 `require.resolve`) |
12
12
  | `$CURRENT_PROJECT` | 当前工作目录 | 会话的 projectPath |
13
13
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  | 路径 | 含义 |
8
8
  |------|------|
9
- | `$EVOLCORE_HOME` | 用户数据根。解析顺序:`EVOLCORE_HOME` 环境变量 → cwd 下有 `agents/defaults.json` 则取 cwd → `~/.evolcore`(第二步意味着**包根和数据根可能是同一个目录**) |
9
+ | `$EVOLCORE_HOME` | 用户数据根。解析顺序:`EVOLCORE_HOME` 环境变量 → `~/.evolcore` |
10
10
  | `$PACKAGE_ROOT` | evolcore 包根目录(由 `getPackageRoot()` 从 `import.meta` 推出) |
11
11
  | `$CURRENT_PROJECT` | 当前工作目录 |
12
12
 
@@ -82,7 +82,7 @@ agent 级索引范围:`$CURRENT_PROJECT` + `$AGENT_DIR`。
82
82
  ## Bootstrap(独立使用 ECK 时)
83
83
 
84
84
  无 evolcore 动态注入时,路径确定顺序:
85
- 1. `$EVOLCORE_HOME`:环境变量 → cwd 下有 `agents/defaults.json` 则取 cwd → `~/.evolcore`
85
+ 1. `$EVOLCORE_HOME`:环境变量 → `~/.evolcore`
86
86
  2. `$PACKAGE_ROOT`:evolcore 安装目录(dev 模式下可能与上一步同一个目录)
87
87
  3. 按上面的派生规则构造其余路径
88
88
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolcore",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "AI Agent gateway connecting Claude, Codex, Gemini, and the bundled ecagent runner to messaging channels with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,53 +0,0 @@
1
- /** Per-session gate used only at the next tool boundary. */
2
- export class PauseController {
3
- state = 'running';
4
- waiters = new Set();
5
- requestPause() {
6
- if (this.state !== 'running')
7
- return false;
8
- this.state = 'pause_requested';
9
- return true;
10
- }
11
- isPauseRequested() { return this.state === 'pause_requested'; }
12
- isPaused() { return this.state === 'paused'; }
13
- waitAtToolBoundary(signal) {
14
- if (signal?.aborted)
15
- return Promise.resolve('cancelled');
16
- if (this.state === 'running')
17
- return Promise.resolve('none');
18
- this.state = 'paused';
19
- return new Promise(resolve => {
20
- const waiter = { resolve: () => { }, abort: undefined };
21
- const finish = (result) => {
22
- this.waiters.delete(waiter);
23
- if (waiter.abort)
24
- signal?.removeEventListener('abort', waiter.abort);
25
- resolve(result);
26
- };
27
- waiter.resolve = finish;
28
- if (signal) {
29
- waiter.abort = () => finish('cancelled');
30
- signal.addEventListener('abort', waiter.abort, { once: true });
31
- }
32
- this.waiters.add(waiter);
33
- if (this.state === 'running')
34
- finish('released');
35
- else if (signal?.aborted)
36
- finish('cancelled');
37
- });
38
- }
39
- resume() {
40
- if (this.state === 'running')
41
- return false;
42
- this.state = 'running';
43
- for (const waiter of [...this.waiters])
44
- waiter.resolve('released');
45
- return true;
46
- }
47
- cancel() {
48
- this.state = 'running';
49
- for (const waiter of [...this.waiters])
50
- waiter.resolve('cancelled');
51
- }
52
- clear() { this.cancel(); }
53
- }