evolcore 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/bin/codex-managed-hook.mjs +4 -1
- package/bin/ec-safe-output.js +26 -7
- package/bin/install-codex-managed-hooks.mjs +201 -0
- package/dist/agents/claude-runner.js +105 -11
- package/dist/agents/codex-app-server-client.js +176 -7
- package/dist/agents/codex-runner.js +248 -84
- package/dist/agents/ecagent-runner.js +21 -2
- package/dist/agents/gemini-runner.js +9 -4
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +11 -33
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +176 -55
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +514 -150
- package/dist/channels/daemon.js +2 -0
- package/dist/cli/agent.js +9 -96
- package/dist/cli/aun-commands.js +1 -1
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +39 -24
- package/dist/cli/fs-command.js +65 -19
- package/dist/cli/init.js +77 -8
- package/dist/cli/queue-command.js +42 -0
- package/dist/cli/restart-monitor.js +3 -14
- package/dist/cli/task-context.js +172 -0
- package/dist/cli/trigger-command.js +7 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +7 -0
- package/dist/config/config-manager.js +27 -3
- package/dist/config/contact-book-store.js +7 -2
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -1
- package/dist/core/auth/auth-gateway.js +30 -1
- package/dist/core/auth/authorization-audit.js +47 -0
- package/dist/core/auth/operation-authorizer.js +1 -0
- package/dist/core/auth/operation-catalog.js +10 -1
- package/dist/core/bootstrap-messages.js +9 -2
- package/dist/core/bootstrap-service.js +8 -1
- package/dist/core/command/command-handler.js +201 -51
- package/dist/core/command/menu-catalog.js +4 -0
- package/dist/core/command/menu-handler.js +85 -18
- package/dist/core/command/menu-protocol.js +23 -2
- package/dist/core/command/slash-handler.js +58 -10
- package/dist/core/event-catalog.js +8 -0
- package/dist/core/handoff/runtime.js +12 -1
- package/dist/core/handoff/store.js +5 -1
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +34 -1
- package/dist/core/message/message-bridge.js +68 -13
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-queue.js +267 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +242 -48
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +649 -18
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +21 -0
- package/dist/core/permission/tool-policy.js +266 -56
- package/dist/core/protected-paths.js +107 -2
- package/dist/core/runtime-lock.js +101 -0
- package/dist/core/session/session-manager.js +21 -2
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/index.js +191 -89
- package/dist/ipc.js +16 -3
- package/dist/paths.js +3 -0
- package/dist/response-system/engines/v1/proactive-flow.js +92 -8
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/history.js +42 -7
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +17 -0
- package/dist/utils/logger.js +37 -4
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/templates/roles/admin.json +7 -0
- package/kits/templates/roles/member.json +8 -1
- package/kits/templates/roles/visitor.json +2 -0
- package/package.json +1 -1
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
|
@@ -14,6 +14,66 @@ const EVOLCORE_COMMAND_POSITION_RE = /(?:^|[;&|()`])\s*ec(?=\s|[;&|()<>`]|$)/i;
|
|
|
14
14
|
function trimShellSpacing(value) {
|
|
15
15
|
return value.replace(/^[ \t]+|[ \t]+$/g, '');
|
|
16
16
|
}
|
|
17
|
+
const MANAGED_TMPDIR_TOKEN = '__EC_MANAGED_TMPDIR__';
|
|
18
|
+
/**
|
|
19
|
+
* Preserve the one environment reference allowed in managed EC paths.
|
|
20
|
+
* The regular literal parser still rejects all expansion by default. The
|
|
21
|
+
* privileged EC classifier opts into this token only after the surrounding
|
|
22
|
+
* policy has validated the path against the session-owned TMPDIR.
|
|
23
|
+
*/
|
|
24
|
+
function substituteManagedTmpDirToken(value) {
|
|
25
|
+
let result = '';
|
|
26
|
+
let quote = null;
|
|
27
|
+
for (let index = 0; index < value.length; index++) {
|
|
28
|
+
const char = value[index];
|
|
29
|
+
if (quote === 'single') {
|
|
30
|
+
result += char;
|
|
31
|
+
if (char === "'")
|
|
32
|
+
quote = null;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (quote === 'double') {
|
|
36
|
+
if (char === '\\') {
|
|
37
|
+
result += char;
|
|
38
|
+
if (index + 1 < value.length)
|
|
39
|
+
result += value[++index];
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (char === '"') {
|
|
43
|
+
quote = null;
|
|
44
|
+
result += char;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (char === "'") {
|
|
49
|
+
quote = 'single';
|
|
50
|
+
result += char;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
else if (char === '\\') {
|
|
54
|
+
result += char;
|
|
55
|
+
if (index + 1 < value.length)
|
|
56
|
+
result += value[++index];
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (value.startsWith('${TMPDIR}', index)) {
|
|
60
|
+
result += MANAGED_TMPDIR_TOKEN;
|
|
61
|
+
index += '${TMPDIR}'.length - 1;
|
|
62
|
+
}
|
|
63
|
+
else if (value.startsWith('$TMPDIR', index)
|
|
64
|
+
&& !/[A-Za-z0-9_]/.test(value[index + '$TMPDIR'.length] ?? '')) {
|
|
65
|
+
result += MANAGED_TMPDIR_TOKEN;
|
|
66
|
+
index += '$TMPDIR'.length - 1;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
result += char;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
function restoreManagedTmpDirToken(value, managedTempDir) {
|
|
75
|
+
return value.replaceAll(MANAGED_TMPDIR_TOKEN, managedTempDir ?? '$TMPDIR');
|
|
76
|
+
}
|
|
17
77
|
function isShellSpacing(value) {
|
|
18
78
|
return value === ' ' || value === '\t';
|
|
19
79
|
}
|
|
@@ -41,8 +101,10 @@ function doubleQuotedIssueAt(input, index) {
|
|
|
41
101
|
* expansion, glob, pipeline, command substitution, redirection, or comment.
|
|
42
102
|
* Anything outside this subset is never granted the privileged EC path.
|
|
43
103
|
*/
|
|
44
|
-
export function parseLiteralShellCommand(command) {
|
|
45
|
-
const input = trimShellSpacing(
|
|
104
|
+
export function parseLiteralShellCommand(command, options = {}) {
|
|
105
|
+
const input = trimShellSpacing(options.allowManagedTmpDir
|
|
106
|
+
? substituteManagedTmpDirToken(command)
|
|
107
|
+
: command);
|
|
46
108
|
if (!input)
|
|
47
109
|
return { ok: false, issue: 'empty-command' };
|
|
48
110
|
if (input.includes('\0'))
|
|
@@ -135,11 +197,16 @@ export function parseLiteralShellCommand(command) {
|
|
|
135
197
|
return { ok: false, issue: 'invalid-quote' };
|
|
136
198
|
finishToken();
|
|
137
199
|
return argv.length > 0
|
|
138
|
-
? {
|
|
200
|
+
? {
|
|
201
|
+
ok: true,
|
|
202
|
+
argv: options.allowManagedTmpDir
|
|
203
|
+
? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
|
|
204
|
+
: argv,
|
|
205
|
+
}
|
|
139
206
|
: { ok: false, issue: 'empty-command' };
|
|
140
207
|
}
|
|
141
|
-
export function parseLiteralShellArgv(command) {
|
|
142
|
-
const result = parseLiteralShellCommand(command);
|
|
208
|
+
export function parseLiteralShellArgv(command, options = {}) {
|
|
209
|
+
const result = parseLiteralShellCommand(command, options);
|
|
143
210
|
return result.ok ? result.argv : null;
|
|
144
211
|
}
|
|
145
212
|
/**
|
|
@@ -157,12 +224,12 @@ export function unwrapCodexShellCommandArgv(argv) {
|
|
|
157
224
|
* Codex app-server. This does not make shell composition safe: the nested
|
|
158
225
|
* command still has to pass `parseLiteralShellCommand`.
|
|
159
226
|
*/
|
|
160
|
-
export function parseCodexShellCommandArgv(command) {
|
|
161
|
-
const outer = parseLiteralShellArgv(command);
|
|
227
|
+
export function parseCodexShellCommandArgv(command, options = {}) {
|
|
228
|
+
const outer = parseLiteralShellArgv(command, options);
|
|
162
229
|
if (!outer)
|
|
163
230
|
return null;
|
|
164
231
|
const wrapped = unwrapCodexShellCommandArgv(outer);
|
|
165
|
-
return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped);
|
|
232
|
+
return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped, options);
|
|
166
233
|
}
|
|
167
234
|
/**
|
|
168
235
|
* Resolve a Codex command input while preserving the raw `commandArgv` form
|
|
@@ -173,10 +240,504 @@ export function parseCodexToolCommandArgv(input) {
|
|
|
173
240
|
if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
|
|
174
241
|
const outer = input.commandArgv;
|
|
175
242
|
const wrapped = unwrapCodexShellCommandArgv(outer);
|
|
176
|
-
return wrapped === undefined
|
|
243
|
+
return wrapped === undefined
|
|
244
|
+
? outer
|
|
245
|
+
: parseLiteralShellArgv(wrapped, { allowManagedTmpDir: true });
|
|
177
246
|
}
|
|
178
247
|
const command = typeof input.command === 'string' ? input.command : '';
|
|
179
|
-
return parseCodexShellCommandArgv(command);
|
|
248
|
+
return parseCodexShellCommandArgv(command, { allowManagedTmpDir: true });
|
|
249
|
+
}
|
|
250
|
+
function isSimpleExecStringLiteral(value) {
|
|
251
|
+
const quote = value[0];
|
|
252
|
+
if ((quote !== '"' && quote !== "'") || value.at(-1) !== quote)
|
|
253
|
+
return false;
|
|
254
|
+
let escaped = false;
|
|
255
|
+
for (let index = 1; index < value.length - 1; index++) {
|
|
256
|
+
const char = value[index];
|
|
257
|
+
if (escaped) {
|
|
258
|
+
escaped = false;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (char === '\\') {
|
|
262
|
+
escaped = true;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (char === '\r' || char === '\n')
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
return !escaped;
|
|
269
|
+
}
|
|
270
|
+
function splitTopLevelExecObjectProperties(body) {
|
|
271
|
+
const properties = [];
|
|
272
|
+
let start = 0;
|
|
273
|
+
let curlyDepth = 0;
|
|
274
|
+
let squareDepth = 0;
|
|
275
|
+
let quote = null;
|
|
276
|
+
let escaped = false;
|
|
277
|
+
for (let index = 0; index < body.length; index++) {
|
|
278
|
+
const char = body[index];
|
|
279
|
+
if (quote) {
|
|
280
|
+
if (escaped) {
|
|
281
|
+
escaped = false;
|
|
282
|
+
}
|
|
283
|
+
else if (char === '\\') {
|
|
284
|
+
escaped = true;
|
|
285
|
+
}
|
|
286
|
+
else if (char === quote) {
|
|
287
|
+
quote = null;
|
|
288
|
+
}
|
|
289
|
+
else if (char === '\r' || char === '\n') {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
295
|
+
quote = char;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (char === '{')
|
|
299
|
+
curlyDepth++;
|
|
300
|
+
else if (char === '}') {
|
|
301
|
+
if (curlyDepth === 0)
|
|
302
|
+
return null;
|
|
303
|
+
curlyDepth--;
|
|
304
|
+
}
|
|
305
|
+
else if (char === '[')
|
|
306
|
+
squareDepth++;
|
|
307
|
+
else if (char === ']') {
|
|
308
|
+
if (squareDepth === 0)
|
|
309
|
+
return null;
|
|
310
|
+
squareDepth--;
|
|
311
|
+
}
|
|
312
|
+
else if (char === ',' && curlyDepth === 0 && squareDepth === 0) {
|
|
313
|
+
properties.push(body.slice(start, index));
|
|
314
|
+
start = index + 1;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (quote || curlyDepth !== 0 || squareDepth !== 0 || escaped)
|
|
318
|
+
return null;
|
|
319
|
+
properties.push(body.slice(start));
|
|
320
|
+
return properties;
|
|
321
|
+
}
|
|
322
|
+
function topLevelExecPropertyColon(property) {
|
|
323
|
+
let curlyDepth = 0;
|
|
324
|
+
let squareDepth = 0;
|
|
325
|
+
let quote = null;
|
|
326
|
+
let escaped = false;
|
|
327
|
+
for (let index = 0; index < property.length; index++) {
|
|
328
|
+
const char = property[index];
|
|
329
|
+
if (quote) {
|
|
330
|
+
if (escaped)
|
|
331
|
+
escaped = false;
|
|
332
|
+
else if (char === '\\')
|
|
333
|
+
escaped = true;
|
|
334
|
+
else if (char === quote)
|
|
335
|
+
quote = null;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
339
|
+
quote = char;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (char === '{')
|
|
343
|
+
curlyDepth++;
|
|
344
|
+
else if (char === '}')
|
|
345
|
+
curlyDepth--;
|
|
346
|
+
else if (char === '[')
|
|
347
|
+
squareDepth++;
|
|
348
|
+
else if (char === ']')
|
|
349
|
+
squareDepth--;
|
|
350
|
+
else if (char === ':' && curlyDepth === 0 && squareDepth === 0)
|
|
351
|
+
return index;
|
|
352
|
+
}
|
|
353
|
+
return -1;
|
|
354
|
+
}
|
|
355
|
+
function parseSimpleExecPropertyKey(raw) {
|
|
356
|
+
const value = raw.trim();
|
|
357
|
+
if (/^[A-Za-z_$][\w$]*$/.test(value))
|
|
358
|
+
return value;
|
|
359
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
360
|
+
try {
|
|
361
|
+
const parsed = JSON.parse(value);
|
|
362
|
+
return typeof parsed === 'string' ? parsed : null;
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (value.startsWith("'") && value.endsWith("'") && /^'[A-Za-z_$][\w$]*'$/.test(value)) {
|
|
369
|
+
return value.slice(1, -1);
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
function validateSimpleExecCarrierObject(source, objectStart, objectEnd) {
|
|
374
|
+
const properties = splitTopLevelExecObjectProperties(source.slice(objectStart + 1, objectEnd - 1));
|
|
375
|
+
if (!properties)
|
|
376
|
+
return false;
|
|
377
|
+
const seen = new Set();
|
|
378
|
+
for (const property of properties) {
|
|
379
|
+
if (!property.trim())
|
|
380
|
+
continue;
|
|
381
|
+
const colon = topLevelExecPropertyColon(property);
|
|
382
|
+
if (colon < 0)
|
|
383
|
+
return false;
|
|
384
|
+
const key = parseSimpleExecPropertyKey(property.slice(0, colon));
|
|
385
|
+
const value = property.slice(colon + 1).trim();
|
|
386
|
+
if (!key || seen.has(key) || !value)
|
|
387
|
+
return false;
|
|
388
|
+
seen.add(key);
|
|
389
|
+
// The command and all carrier options must be inert literals. In
|
|
390
|
+
// particular, reject calls, member expressions, template literals, and
|
|
391
|
+
// nested objects that could execute while the real tool is invoked.
|
|
392
|
+
if (key === 'cmd') {
|
|
393
|
+
if (!isSimpleExecStringLiteral(value))
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
else if (!isSimpleExecStringLiteral(value)
|
|
397
|
+
&& !/^(?:true|false|null)$/.test(value)
|
|
398
|
+
&& !/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(value)) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return seen.has('cmd');
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Extract the one literal command carried by Codex's custom `exec` tool.
|
|
406
|
+
*
|
|
407
|
+
* The desktop Codex tool is a JavaScript carrier around `tools.exec_command`.
|
|
408
|
+
* Treating the carrier as a send command is safe only when it contains one
|
|
409
|
+
* direct command call; a script containing a send plus any other command must
|
|
410
|
+
* remain blocked because its execution order cannot satisfy the gate.
|
|
411
|
+
*/
|
|
412
|
+
export function parseCodexExecToolCommand(input) {
|
|
413
|
+
const source = typeof input.input === 'string' ? input.input : '';
|
|
414
|
+
if (!source)
|
|
415
|
+
return null;
|
|
416
|
+
// Count only source-level calls. A command body is itself a string literal,
|
|
417
|
+
// so a message such as `"mention tools.foo("` must not look like a second
|
|
418
|
+
// tool invocation. Comments are ignored for the same reason.
|
|
419
|
+
const toolCalls = [];
|
|
420
|
+
let sourceQuote = null;
|
|
421
|
+
let sourceEscaped = false;
|
|
422
|
+
let lineComment = false;
|
|
423
|
+
let blockComment = false;
|
|
424
|
+
for (let index = 0; index < source.length;) {
|
|
425
|
+
const char = source[index];
|
|
426
|
+
const next = source[index + 1];
|
|
427
|
+
if (lineComment) {
|
|
428
|
+
if (char === '\n' || char === '\r')
|
|
429
|
+
lineComment = false;
|
|
430
|
+
index++;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (blockComment) {
|
|
434
|
+
if (char === '*' && next === '/') {
|
|
435
|
+
blockComment = false;
|
|
436
|
+
index += 2;
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
index++;
|
|
440
|
+
}
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (sourceQuote) {
|
|
444
|
+
if (sourceEscaped) {
|
|
445
|
+
sourceEscaped = false;
|
|
446
|
+
}
|
|
447
|
+
else if (char === '\\') {
|
|
448
|
+
sourceEscaped = true;
|
|
449
|
+
}
|
|
450
|
+
else if (char === sourceQuote) {
|
|
451
|
+
sourceQuote = null;
|
|
452
|
+
}
|
|
453
|
+
index++;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (char === '/' && next === '/') {
|
|
457
|
+
lineComment = true;
|
|
458
|
+
index += 2;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (char === '/' && next === '*') {
|
|
462
|
+
blockComment = true;
|
|
463
|
+
index += 2;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
467
|
+
sourceQuote = char;
|
|
468
|
+
index++;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (source.startsWith('tools.', index)
|
|
472
|
+
&& (index === 0 || !/[A-Za-z0-9_$]/.test(source[index - 1]))) {
|
|
473
|
+
let nameEnd = index + 'tools.'.length;
|
|
474
|
+
const nameMatch = source.slice(nameEnd).match(/^[A-Za-z_$][\w$]*/);
|
|
475
|
+
if (nameMatch) {
|
|
476
|
+
nameEnd += nameMatch[0].length;
|
|
477
|
+
while (nameEnd < source.length && /\s/u.test(source[nameEnd]))
|
|
478
|
+
nameEnd++;
|
|
479
|
+
if (source[nameEnd] === '(') {
|
|
480
|
+
toolCalls.push({ name: nameMatch[0], start: index, openParen: nameEnd });
|
|
481
|
+
index = nameEnd + 1;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
index++;
|
|
487
|
+
}
|
|
488
|
+
const calls = toolCalls.filter(call => call.name === 'exec_command');
|
|
489
|
+
if (toolCalls.length !== 1 || calls.length !== 1)
|
|
490
|
+
return null;
|
|
491
|
+
const callStart = calls[0].start;
|
|
492
|
+
const callOpenParen = calls[0].openParen;
|
|
493
|
+
const prefix = source.slice(0, callStart);
|
|
494
|
+
const binding = prefix.match(/^\s*(?:(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s*|await\s*)$/);
|
|
495
|
+
if (!binding)
|
|
496
|
+
return null;
|
|
497
|
+
const outputBinding = binding[1];
|
|
498
|
+
let objectStart = callOpenParen + 1;
|
|
499
|
+
while (objectStart < source.length && /\s/u.test(source[objectStart]))
|
|
500
|
+
objectStart++;
|
|
501
|
+
if (source[objectStart] !== '{')
|
|
502
|
+
return null;
|
|
503
|
+
// Desktop Codex serializes the carrier object as JSON (`{"cmd":"..."}`),
|
|
504
|
+
// while hand-written calls may use a JavaScript identifier (`{ cmd: "..." }`).
|
|
505
|
+
// Walk the carrier object so a nested `{ cmd: ... }` or a string containing
|
|
506
|
+
// `cmd:` cannot be mistaken for the direct command property.
|
|
507
|
+
let curlyDepth = 0;
|
|
508
|
+
let squareDepth = 0;
|
|
509
|
+
let propertyStart = false;
|
|
510
|
+
let scanQuote = null;
|
|
511
|
+
let scanEscaped = false;
|
|
512
|
+
let valueStart = -1;
|
|
513
|
+
let objectEnd = -1;
|
|
514
|
+
for (let index = objectStart; index < source.length; index++) {
|
|
515
|
+
const char = source[index];
|
|
516
|
+
if (!scanQuote && curlyDepth === 1 && squareDepth === 0 && propertyStart) {
|
|
517
|
+
let keyStart = index;
|
|
518
|
+
while (keyStart < source.length && /\s/u.test(source[keyStart]))
|
|
519
|
+
keyStart++;
|
|
520
|
+
let key;
|
|
521
|
+
let keyEnd = keyStart;
|
|
522
|
+
const keyQuote = source[keyStart];
|
|
523
|
+
if (keyQuote === '"' || keyQuote === "'") {
|
|
524
|
+
keyEnd++;
|
|
525
|
+
let keyEscaped = false;
|
|
526
|
+
for (; keyEnd < source.length; keyEnd++) {
|
|
527
|
+
const keyChar = source[keyEnd];
|
|
528
|
+
if (keyEscaped) {
|
|
529
|
+
keyEscaped = false;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (keyChar === '\\') {
|
|
533
|
+
keyEscaped = true;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (keyChar === keyQuote)
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
if (keyEnd >= source.length)
|
|
540
|
+
return null;
|
|
541
|
+
key = source.slice(keyStart + 1, keyEnd);
|
|
542
|
+
keyEnd++;
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
const keyMatch = source.slice(keyStart).match(/^[A-Za-z_$][\w$]*/);
|
|
546
|
+
if (!keyMatch) {
|
|
547
|
+
propertyStart = false;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
key = keyMatch[0];
|
|
551
|
+
keyEnd += key.length;
|
|
552
|
+
}
|
|
553
|
+
while (keyEnd < source.length && /\s/u.test(source[keyEnd]))
|
|
554
|
+
keyEnd++;
|
|
555
|
+
if (source[keyEnd] !== ':') {
|
|
556
|
+
propertyStart = false;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
keyEnd++;
|
|
560
|
+
while (keyEnd < source.length && /\s/u.test(source[keyEnd]))
|
|
561
|
+
keyEnd++;
|
|
562
|
+
propertyStart = false;
|
|
563
|
+
if (key === 'cmd') {
|
|
564
|
+
const valueQuote = source[keyEnd];
|
|
565
|
+
if (valueQuote !== '"' && valueQuote !== "'")
|
|
566
|
+
return null;
|
|
567
|
+
if (valueStart >= 0)
|
|
568
|
+
return null;
|
|
569
|
+
valueStart = keyEnd;
|
|
570
|
+
index = keyEnd - 1;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
index = keyEnd - 1;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (scanQuote) {
|
|
577
|
+
if (scanEscaped) {
|
|
578
|
+
scanEscaped = false;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (char === '\\') {
|
|
582
|
+
scanEscaped = true;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (char === scanQuote)
|
|
586
|
+
scanQuote = null;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
590
|
+
scanQuote = char;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (char === '{') {
|
|
594
|
+
curlyDepth++;
|
|
595
|
+
if (curlyDepth === 1)
|
|
596
|
+
propertyStart = true;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (char === '}') {
|
|
600
|
+
if (curlyDepth === 1) {
|
|
601
|
+
objectEnd = index + 1;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
curlyDepth--;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (char === '[') {
|
|
608
|
+
squareDepth++;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (char === ']') {
|
|
612
|
+
squareDepth = Math.max(0, squareDepth - 1);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (curlyDepth !== 1 || squareDepth !== 0)
|
|
616
|
+
continue;
|
|
617
|
+
if (char === ',') {
|
|
618
|
+
propertyStart = true;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (valueStart < 0 || objectEnd < 0)
|
|
623
|
+
return null;
|
|
624
|
+
if (!validateSimpleExecCarrierObject(source, objectStart, objectEnd))
|
|
625
|
+
return null;
|
|
626
|
+
let callEnd = objectEnd;
|
|
627
|
+
while (callEnd < source.length && /\s/u.test(source[callEnd]))
|
|
628
|
+
callEnd++;
|
|
629
|
+
if (source[callEnd] !== ')')
|
|
630
|
+
return null;
|
|
631
|
+
callEnd++;
|
|
632
|
+
// The carrier is intentionally restricted to one top-level await. This
|
|
633
|
+
// rejects loops/conditionals that could invoke the same source call more
|
|
634
|
+
// than once, and prevents additional arguments from hiding another action.
|
|
635
|
+
const tail = source.slice(callEnd).replace(/\/\/[^\r\n]*|\/\*[\s\S]*?\*\//g, '');
|
|
636
|
+
const tailPattern = outputBinding
|
|
637
|
+
? new RegExp(`^\\s*;?\\s*(?:text\\(\\s*${outputBinding}\\.output\\s*\\)\\s*;?)?\\s*$`)
|
|
638
|
+
: /^\s*;?\s*$/;
|
|
639
|
+
if (!tailPattern.test(tail))
|
|
640
|
+
return null;
|
|
641
|
+
const valueQuote = source[valueStart];
|
|
642
|
+
const quoteForValue = valueQuote === '"' || valueQuote === "'" ? valueQuote : null;
|
|
643
|
+
if (!quoteForValue)
|
|
644
|
+
return null;
|
|
645
|
+
const quote = quoteForValue;
|
|
646
|
+
let end = valueStart + 1;
|
|
647
|
+
let escaped = false;
|
|
648
|
+
for (; end < source.length; end++) {
|
|
649
|
+
const char = source[end];
|
|
650
|
+
if (escaped) {
|
|
651
|
+
escaped = false;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (char === '\\') {
|
|
655
|
+
escaped = true;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (char === quote)
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
if (end >= source.length)
|
|
662
|
+
return null;
|
|
663
|
+
const raw = source.slice(valueStart, end + 1);
|
|
664
|
+
if (quote === '"') {
|
|
665
|
+
try {
|
|
666
|
+
return JSON.parse(raw);
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// Decode the JS string escapes needed for command literals without
|
|
673
|
+
// evaluating model-provided source code. Unknown escapes are rejected:
|
|
674
|
+
// silently dropping the backslash could make the parsed command differ
|
|
675
|
+
// from the string that JavaScript will actually execute (for example
|
|
676
|
+
// `\x26` becoming `&`).
|
|
677
|
+
let decoded = '';
|
|
678
|
+
for (let index = 1; index < raw.length - 1; index++) {
|
|
679
|
+
const char = raw[index];
|
|
680
|
+
if (char !== '\\') {
|
|
681
|
+
decoded += char;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const next = raw[++index];
|
|
685
|
+
if (next === undefined)
|
|
686
|
+
return null;
|
|
687
|
+
if (next === 'n')
|
|
688
|
+
decoded += '\n';
|
|
689
|
+
else if (next === 'r')
|
|
690
|
+
decoded += '\r';
|
|
691
|
+
else if (next === 't')
|
|
692
|
+
decoded += '\t';
|
|
693
|
+
else if (next === 'b')
|
|
694
|
+
decoded += '\b';
|
|
695
|
+
else if (next === 'f')
|
|
696
|
+
decoded += '\f';
|
|
697
|
+
else if (next === 'v')
|
|
698
|
+
decoded += '\v';
|
|
699
|
+
else if (next === '0') {
|
|
700
|
+
if (/^[0-9]/.test(raw[index + 1] ?? ''))
|
|
701
|
+
return null;
|
|
702
|
+
decoded += '\0';
|
|
703
|
+
}
|
|
704
|
+
else if (next === 'x') {
|
|
705
|
+
const hex = raw.slice(index + 1, index + 3);
|
|
706
|
+
if (!/^[0-9a-fA-F]{2}$/.test(hex))
|
|
707
|
+
return null;
|
|
708
|
+
decoded += String.fromCharCode(parseInt(hex, 16));
|
|
709
|
+
index += 2;
|
|
710
|
+
}
|
|
711
|
+
else if (next === 'u') {
|
|
712
|
+
const hex = raw.slice(index + 1, index + 5);
|
|
713
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex))
|
|
714
|
+
return null;
|
|
715
|
+
decoded += String.fromCharCode(parseInt(hex, 16));
|
|
716
|
+
index += 4;
|
|
717
|
+
}
|
|
718
|
+
else if (next === '\n') {
|
|
719
|
+
// JavaScript line continuations contribute no command character.
|
|
720
|
+
}
|
|
721
|
+
else if (next === '\r') {
|
|
722
|
+
if (raw[index + 1] === '\n')
|
|
723
|
+
index++;
|
|
724
|
+
}
|
|
725
|
+
else if (next === '\\' || next === "'" || next === '"' || next === '`' || next === '/') {
|
|
726
|
+
decoded += next;
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return decoded;
|
|
733
|
+
}
|
|
734
|
+
function parseProactiveToolCommandArgv(toolName, input) {
|
|
735
|
+
if (toolName === 'Bash' || toolName === 'Shell')
|
|
736
|
+
return parseCodexToolCommandArgv(input);
|
|
737
|
+
if (toolName !== 'exec')
|
|
738
|
+
return null;
|
|
739
|
+
const command = parseCodexExecToolCommand(input);
|
|
740
|
+
return command ? parseCodexShellCommandArgv(command, { allowManagedTmpDir: true }) : null;
|
|
180
741
|
}
|
|
181
742
|
function scanBoundedOutputOperators(command) {
|
|
182
743
|
const operators = [];
|
|
@@ -290,7 +851,7 @@ function parseStdinOnlyHeadArgs(segment) {
|
|
|
290
851
|
* optional stderr merge, one bounded `head`, and one create-only output path.
|
|
291
852
|
*/
|
|
292
853
|
export function parseBoundedOutputShellCommand(command) {
|
|
293
|
-
const input = trimShellSpacing(command);
|
|
854
|
+
const input = trimShellSpacing(substituteManagedTmpDirToken(command));
|
|
294
855
|
if (!input)
|
|
295
856
|
return null;
|
|
296
857
|
const operators = scanBoundedOutputOperators(input);
|
|
@@ -311,6 +872,10 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
311
872
|
const argv = parseLiteralShellArgv(sourceText);
|
|
312
873
|
if (!argv)
|
|
313
874
|
return null;
|
|
875
|
+
// `$TMPDIR` is a transport-level path reference, never an argument
|
|
876
|
+
// expansion for the privileged command itself.
|
|
877
|
+
if (argv.some(value => value.includes(MANAGED_TMPDIR_TOKEN)))
|
|
878
|
+
return null;
|
|
314
879
|
let mergeStderr = false;
|
|
315
880
|
let pipe;
|
|
316
881
|
let redirect;
|
|
@@ -345,7 +910,12 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
345
910
|
const outputArgv = parseLiteralShellArgv(trimShellSpacing(input.slice(redirect.end)));
|
|
346
911
|
if (!outputArgv || outputArgv.length !== 1)
|
|
347
912
|
return null;
|
|
348
|
-
|
|
913
|
+
// A single-quoted or escaped `$TMPDIR` is literal shell text, not the
|
|
914
|
+
// managed directory reference. Do not reinterpret it as a route to the
|
|
915
|
+
// session temp root.
|
|
916
|
+
if (outputArgv[0].includes('$') && !outputArgv[0].includes(MANAGED_TMPDIR_TOKEN))
|
|
917
|
+
return null;
|
|
918
|
+
outputPath = restoreManagedTmpDirToken(outputArgv[0]);
|
|
349
919
|
}
|
|
350
920
|
return { argv, mergeStderr, ...(headArgs ? { headArgs } : {}), ...(outputPath ? { outputPath } : {}) };
|
|
351
921
|
}
|
|
@@ -355,13 +925,14 @@ export function parseBoundedOutputShellCommand(command) {
|
|
|
355
925
|
* invocation may leave the sandbox, while shell composition may not.
|
|
356
926
|
*/
|
|
357
927
|
export function classifyEvolcoreShellCommand(command) {
|
|
358
|
-
const
|
|
928
|
+
const parseOptions = { allowManagedTmpDir: true };
|
|
929
|
+
const literal = parseLiteralShellCommand(command, parseOptions);
|
|
359
930
|
if (literal.ok) {
|
|
360
931
|
if (literal.argv[0] === 'ec')
|
|
361
932
|
return { kind: 'literal' };
|
|
362
933
|
const wrapped = unwrapCodexShellCommandArgv(literal.argv);
|
|
363
934
|
if (wrapped !== undefined) {
|
|
364
|
-
const nested = parseLiteralShellCommand(wrapped);
|
|
935
|
+
const nested = parseLiteralShellCommand(wrapped, parseOptions);
|
|
365
936
|
if (nested.ok && nested.argv[0] === 'ec')
|
|
366
937
|
return { kind: 'literal' };
|
|
367
938
|
if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(wrapped)) {
|
|
@@ -410,6 +981,66 @@ const PROACTIVE_SEND_VALUE_FLAGS = new Set([
|
|
|
410
981
|
const PROACTIVE_SEND_BOOLEAN_FLAGS = new Set([
|
|
411
982
|
'--encrypt', '--no-encrypt', '--mention-all',
|
|
412
983
|
]);
|
|
984
|
+
const SEND_CONTENT_VALUE_FLAGS = new Set([
|
|
985
|
+
'--payload', '--title', '--description', '--text', '--transcript',
|
|
986
|
+
]);
|
|
987
|
+
const TRIGGER_CONTENT_VALUE_FLAGS = new Set(['--prompt']);
|
|
988
|
+
/**
|
|
989
|
+
* A shell-escaped `$TMPDIR` in a msg/group text body or trigger prompt is
|
|
990
|
+
* ordinary content, not a path reference. Keep every other unresolved
|
|
991
|
+
* occurrence fail-closed so file and control arguments cannot bypass
|
|
992
|
+
* managed-path checks.
|
|
993
|
+
*/
|
|
994
|
+
export function containsLiteralManagedTmpDirOutsideSendContent(argv) {
|
|
995
|
+
const literalIndexes = argv
|
|
996
|
+
.map((value, index) => value.includes('$TMPDIR') ? index : -1)
|
|
997
|
+
.filter(index => index >= 0);
|
|
998
|
+
if (literalIndexes.length === 0)
|
|
999
|
+
return false;
|
|
1000
|
+
const isSendCommand = argv[0] === 'ec'
|
|
1001
|
+
&& (argv[1] === 'msg' || argv[1] === 'group')
|
|
1002
|
+
&& argv[2] === 'send';
|
|
1003
|
+
const isTriggerCommand = argv[0] === 'ec' && argv[1] === 'trigger';
|
|
1004
|
+
if (!isSendCommand && !isTriggerCommand)
|
|
1005
|
+
return true;
|
|
1006
|
+
const textIndexes = new Set();
|
|
1007
|
+
if (isTriggerCommand) {
|
|
1008
|
+
for (let index = 2; index < argv.length; index++) {
|
|
1009
|
+
if (!TRIGGER_CONTENT_VALUE_FLAGS.has(argv[index]))
|
|
1010
|
+
continue;
|
|
1011
|
+
if (argv[index + 1] !== undefined)
|
|
1012
|
+
textIndexes.add(index + 1);
|
|
1013
|
+
index++;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (!isSendCommand)
|
|
1017
|
+
return literalIndexes.some(index => !textIndexes.has(index));
|
|
1018
|
+
let endOfOptions = false;
|
|
1019
|
+
for (let index = 5; index < argv.length; index++) {
|
|
1020
|
+
const value = argv[index];
|
|
1021
|
+
if (endOfOptions) {
|
|
1022
|
+
textIndexes.add(index);
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (value === '--') {
|
|
1026
|
+
endOfOptions = true;
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (PROACTIVE_SEND_BOOLEAN_FLAGS.has(value))
|
|
1030
|
+
continue;
|
|
1031
|
+
if (PROACTIVE_SEND_VALUE_FLAGS.has(value)) {
|
|
1032
|
+
const operandIndex = ++index;
|
|
1033
|
+
if (SEND_CONTENT_VALUE_FLAGS.has(value) && argv[operandIndex] !== undefined) {
|
|
1034
|
+
textIndexes.add(operandIndex);
|
|
1035
|
+
}
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
textIndexes.add(index);
|
|
1039
|
+
}
|
|
1040
|
+
if (textIndexes.size === 0)
|
|
1041
|
+
return true;
|
|
1042
|
+
return literalIndexes.some(index => !textIndexes.has(index));
|
|
1043
|
+
}
|
|
413
1044
|
function hasExecutableProactiveSendPayload(argv, scope) {
|
|
414
1045
|
if (!argv || argv[0] !== 'ec' || argv[1] !== scope || argv[2] !== 'send')
|
|
415
1046
|
return false;
|
|
@@ -531,11 +1162,11 @@ export function isEvolcoreSendCommandForSession(toolName, input, channelId) {
|
|
|
531
1162
|
}
|
|
532
1163
|
/** Proactive first-send guard: exact channel kind and current target only. */
|
|
533
1164
|
export function isExactEvolcoreSendCommandForSession(toolName, input, channelId, chatType, selfAid) {
|
|
534
|
-
// Codex
|
|
535
|
-
//
|
|
536
|
-
if (
|
|
1165
|
+
// Codex app-server exposes shell items as `Shell`; the desktop custom tool
|
|
1166
|
+
// is represented as `exec` and is handled only for one literal inner call.
|
|
1167
|
+
if (!selfAid)
|
|
537
1168
|
return false;
|
|
538
|
-
const argv =
|
|
1169
|
+
const argv = parseProactiveToolCommandArgv(toolName, input);
|
|
539
1170
|
const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
|
|
540
1171
|
if (!parsed || parsed.action !== 'send')
|
|
541
1172
|
return false;
|