evolcore 0.0.13 → 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 +15 -0
- package/bin/codex-managed-hook.mjs +4 -1
- package/bin/install-codex-managed-hooks.mjs +201 -0
- package/dist/agents/claude-runner.js +53 -5
- package/dist/agents/codex-app-server-client.js +123 -2
- package/dist/agents/codex-runner.js +149 -30
- package/dist/agents/ecagent-runner.js +17 -1
- package/dist/agents/gemini-runner.js +9 -4
- package/dist/aun/msg/managed-operation.js +63 -3
- package/dist/channels/aun.js +144 -15
- package/dist/channels/daemon.js +2 -0
- package/dist/cli/aun-commands.js +1 -1
- package/dist/cli/fs-command.js +46 -9
- package/dist/cli/task-context.js +172 -0
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/config-manager.js +6 -2
- package/dist/config/contact-book-store.js +7 -2
- package/dist/core/auth/auth-gateway.js +1 -0
- package/dist/core/auth/authorization-audit.js +32 -0
- package/dist/core/auth/operation-catalog.js +3 -3
- package/dist/core/bootstrap-service.js +7 -1
- package/dist/core/command/command-handler.js +3 -0
- package/dist/core/event-catalog.js +2 -0
- package/dist/core/message/im-renderer.js +15 -1
- package/dist/core/message/message-bridge.js +5 -2
- package/dist/core/message/response-engine.js +138 -10
- package/dist/core/permission/ec-command-parser.js +556 -4
- package/dist/core/permission/tool-policy.js +17 -29
- package/dist/core/runtime-lock.js +101 -0
- package/dist/index.js +30 -3
- 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/history.js +42 -7
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/logger.js +37 -4
- package/kits/templates/roles/admin.json +2 -0
- package/kits/templates/roles/member.json +1 -0
- package/package.json +1 -1
|
@@ -247,6 +247,498 @@ export function parseCodexToolCommandArgv(input) {
|
|
|
247
247
|
const command = typeof input.command === 'string' ? input.command : '';
|
|
248
248
|
return parseCodexShellCommandArgv(command, { allowManagedTmpDir: true });
|
|
249
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;
|
|
741
|
+
}
|
|
250
742
|
function scanBoundedOutputOperators(command) {
|
|
251
743
|
const operators = [];
|
|
252
744
|
let quote = null;
|
|
@@ -489,6 +981,66 @@ const PROACTIVE_SEND_VALUE_FLAGS = new Set([
|
|
|
489
981
|
const PROACTIVE_SEND_BOOLEAN_FLAGS = new Set([
|
|
490
982
|
'--encrypt', '--no-encrypt', '--mention-all',
|
|
491
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
|
+
}
|
|
492
1044
|
function hasExecutableProactiveSendPayload(argv, scope) {
|
|
493
1045
|
if (!argv || argv[0] !== 'ec' || argv[1] !== scope || argv[2] !== 'send')
|
|
494
1046
|
return false;
|
|
@@ -610,11 +1162,11 @@ export function isEvolcoreSendCommandForSession(toolName, input, channelId) {
|
|
|
610
1162
|
}
|
|
611
1163
|
/** Proactive first-send guard: exact channel kind and current target only. */
|
|
612
1164
|
export function isExactEvolcoreSendCommandForSession(toolName, input, channelId, chatType, selfAid) {
|
|
613
|
-
// Codex
|
|
614
|
-
//
|
|
615
|
-
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)
|
|
616
1168
|
return false;
|
|
617
|
-
const argv =
|
|
1169
|
+
const argv = parseProactiveToolCommandArgv(toolName, input);
|
|
618
1170
|
const parsed = argv ? parseEvolcoreSendArgv(argv) : null;
|
|
619
1171
|
if (!parsed || parsed.action !== 'send')
|
|
620
1172
|
return false;
|
|
@@ -1348,40 +1348,28 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
1348
1348
|
return prepareBoundedOutputInput(input, ecCommand.command, 'ec', context);
|
|
1349
1349
|
}
|
|
1350
1350
|
if (ecCommand.kind === 'composite') {
|
|
1351
|
-
|
|
1352
|
-
// `/bin/bash -lc ...` command string. A composite inside that carrier
|
|
1353
|
-
// is not eligible for a delegation ticket, but owner bypass still
|
|
1354
|
-
// handles it as an ordinary shell request. Structured `commandArgv`
|
|
1355
|
-
// callbacks remain fail-closed below because their command boundary is
|
|
1356
|
-
// authoritative.
|
|
1357
|
-
const legacyRenderedCarrier = !explicitCommandArgv && wrappedCommand !== undefined;
|
|
1358
|
-
const allowLegacyBypassComposition = legacyRenderedCarrier
|
|
1359
|
-
&& context.permissionMode === 'bypass'
|
|
1360
|
-
&& ecCommand.issue === 'shell-composition';
|
|
1361
|
-
if (!allowLegacyBypassComposition) {
|
|
1362
|
-
if (ecCommand.issue === 'unsafe-expansion') {
|
|
1363
|
-
return {
|
|
1364
|
-
behavior: 'deny',
|
|
1365
|
-
input,
|
|
1366
|
-
message: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量',
|
|
1367
|
-
policyCode: 'ec_shell_unsafe_expansion',
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
1370
|
-
if (ecCommand.issue === 'invalid-quote') {
|
|
1371
|
-
return {
|
|
1372
|
-
behavior: 'deny',
|
|
1373
|
-
input,
|
|
1374
|
-
message: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"',
|
|
1375
|
-
policyCode: 'ec_shell_invalid_quote',
|
|
1376
|
-
};
|
|
1377
|
-
}
|
|
1351
|
+
if (ecCommand.issue === 'unsafe-expansion') {
|
|
1378
1352
|
return {
|
|
1379
1353
|
behavior: 'deny',
|
|
1380
1354
|
input,
|
|
1381
|
-
message: '🔒 EC
|
|
1382
|
-
policyCode: '
|
|
1355
|
+
message: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量',
|
|
1356
|
+
policyCode: 'ec_shell_unsafe_expansion',
|
|
1383
1357
|
};
|
|
1384
1358
|
}
|
|
1359
|
+
if (ecCommand.issue === 'invalid-quote') {
|
|
1360
|
+
return {
|
|
1361
|
+
behavior: 'deny',
|
|
1362
|
+
input,
|
|
1363
|
+
message: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"',
|
|
1364
|
+
policyCode: 'ec_shell_invalid_quote',
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
return {
|
|
1368
|
+
behavior: 'deny',
|
|
1369
|
+
input,
|
|
1370
|
+
message: '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。',
|
|
1371
|
+
policyCode: 'ec_shell_composite_command',
|
|
1372
|
+
};
|
|
1385
1373
|
}
|
|
1386
1374
|
const boundedOutput = parseBoundedOutputShellCommand(command);
|
|
1387
1375
|
const hostProcessCommand = boundedOutput
|