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 +19 -0
- package/dist/agents/codex-runner.js +22 -18
- package/dist/cli/daemon-commands.js +29 -2
- package/dist/core/data-migration.js +10 -4
- package/dist/core/message/message-bridge.js +6 -11
- package/dist/core/message/response-engine.js +53 -2
- package/dist/core/permission/ec-command-parser.js +148 -22
- package/dist/core/permission/tool-policy.js +18 -6
- package/dist/ipc.js +6 -1
- package/dist/paths.js +0 -3
- package/kits/docs/path-registry.md +1 -1
- package/kits/rules/02-navigation.md +2 -2
- package/package.json +1 -1
- package/dist/core/message/pause-controller.js +0 -53
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,
|
|
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
|
|
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
|
|
347
|
-
?
|
|
348
|
+
const explicitCarrier = explicitArgv
|
|
349
|
+
? resolveCodexShellCarrierArgv(explicitArgv)
|
|
348
350
|
: undefined;
|
|
349
|
-
if (
|
|
350
|
-
return this.approvedLiteralEvolcoreCommandArgv(
|
|
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
|
|
361
|
-
?
|
|
362
|
+
const shellCarrier = wrappedArgv
|
|
363
|
+
? resolveCodexShellCarrierArgv(wrappedArgv)
|
|
362
364
|
: undefined;
|
|
363
|
-
return
|
|
365
|
+
return shellCarrier === undefined
|
|
364
366
|
? undefined
|
|
365
|
-
: this.approvedLiteralEvolcoreCommandArgv(
|
|
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
|
|
375
|
-
return
|
|
376
|
-
&& classifyEvolcoreShellCommand(
|
|
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(
|
|
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
|
|
1299
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
|
5
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
232
|
-
return
|
|
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
|
|
243
|
-
return
|
|
360
|
+
const carrier = resolveCodexShellCarrierArgv(outer);
|
|
361
|
+
return carrier === undefined
|
|
244
362
|
? outer
|
|
245
|
-
: parseLiteralShellArgv(
|
|
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
|
|
934
|
-
if (
|
|
935
|
-
const nested = parseLiteralShellCommand(
|
|
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(
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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` →
|
|
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` 环境变量 →
|
|
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`:环境变量 →
|
|
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.
|
|
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
|
-
}
|