@ouro.bot/cli 0.1.0-alpha.730 → 0.1.0-alpha.732
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/README.md +8 -0
- package/changelog.json +18 -0
- package/dist/heart/daemon/cli-exec.js +38 -4
- package/dist/heart/daemon/cli-help.js +9 -4
- package/dist/heart/daemon/cli-parse.js +30 -6
- package/dist/heart/daemon/doctor.js +294 -28
- package/dist/heart/daemon/freshness.js +4 -2
- package/dist/heart/daemon/logs-prune.js +26 -4
- package/dist/heart/daemon/prunable-bundle.js +144 -0
- package/dist/mailroom/blob-store.js +102 -6
- package/dist/mailroom/cache-sync-cli.js +54 -0
- package/dist/mailroom/hosted-cache-sync.js +249 -0
- package/dist/mailroom/reader.js +59 -0
- package/dist/mailroom/search-cache.js +76 -6
- package/dist/repertoire/guardrails.js +674 -0
- package/dist/repertoire/tools-mail.js +5 -152
- package/package.json +1 -1
|
@@ -38,6 +38,7 @@ exports.guardInvocation = guardInvocation;
|
|
|
38
38
|
const fs = __importStar(require("node:fs"));
|
|
39
39
|
const path = __importStar(require("node:path"));
|
|
40
40
|
const friends_1 = require("@ouro.bot/friends");
|
|
41
|
+
const shell_quote_1 = require("shell-quote");
|
|
41
42
|
const runtime_1 = require("../nerves/runtime");
|
|
42
43
|
const store_1 = require("../commerce/store");
|
|
43
44
|
const deny = (reason) => ({ allowed: false, reason });
|
|
@@ -298,6 +299,676 @@ const COMMERCE_FAMILY_TOOLS = new Set(["commerce_checkout_preview", "commerce_ch
|
|
|
298
299
|
const COMMERCE_AUTHORITY_TOOLS = new Set(["stripe_create_card", "flight_hold", "flight_book"]);
|
|
299
300
|
const MAIL_FAMILY_TOOLS = new Set(["mail_screener", "mail_decide", "mail_access_log", "mail_send", "mail_index_refresh"]);
|
|
300
301
|
const MAIL_DELEGATED_READ_TOOLS = new Set(["mail_recent", "mail_search"]);
|
|
302
|
+
function opaqueAnsiShellAtoms(command) {
|
|
303
|
+
const atoms = [];
|
|
304
|
+
let inAnsiQuote = false;
|
|
305
|
+
for (let index = 0; index < command.length;) {
|
|
306
|
+
if (!inAnsiQuote && command.startsWith("$'", index)) {
|
|
307
|
+
inAnsiQuote = true;
|
|
308
|
+
index += 2;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const character = command[index];
|
|
312
|
+
if (inAnsiQuote && character === "'") {
|
|
313
|
+
inAnsiQuote = false;
|
|
314
|
+
index += 1;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (inAnsiQuote && character === "\\") {
|
|
318
|
+
const escape = command.slice(index).match(/^\\(?:x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{1,4}|U[0-9a-fA-F]{1,8}|[0-7]{1,3}|c.|.)?/s)[0];
|
|
319
|
+
atoms.push(null);
|
|
320
|
+
index += escape.length;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (!inAnsiQuote && (character === "'" || character === '"')) {
|
|
324
|
+
index += 1;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (!inAnsiQuote && character === "\\" && index + 1 < command.length) {
|
|
328
|
+
atoms.push(command[index + 1]);
|
|
329
|
+
index += 2;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
atoms.push(character);
|
|
333
|
+
index += 1;
|
|
334
|
+
}
|
|
335
|
+
return atoms;
|
|
336
|
+
}
|
|
337
|
+
function shellContainsAmbiguousAnsiMailCacheSync(command) {
|
|
338
|
+
if (!command.includes("$'"))
|
|
339
|
+
return false;
|
|
340
|
+
const expected = ["ouro", "mail", "sync-cache"];
|
|
341
|
+
let states = new Set(["0:0"]);
|
|
342
|
+
const crossTokenBoundary = (current) => {
|
|
343
|
+
const next = new Set(["0:0"]);
|
|
344
|
+
for (const state of current) {
|
|
345
|
+
const [tokenIndex, characterIndex] = state.split(":").map(Number);
|
|
346
|
+
if (characterIndex !== expected[tokenIndex].length)
|
|
347
|
+
continue;
|
|
348
|
+
if (tokenIndex === expected.length - 1)
|
|
349
|
+
return { matched: true, states: next };
|
|
350
|
+
next.add(`${tokenIndex + 1}:0`);
|
|
351
|
+
}
|
|
352
|
+
return { matched: false, states: next };
|
|
353
|
+
};
|
|
354
|
+
for (const atom of opaqueAnsiShellAtoms(command)) {
|
|
355
|
+
if (atom === null) {
|
|
356
|
+
const next = new Set(states);
|
|
357
|
+
for (const state of states) {
|
|
358
|
+
const [tokenIndex, characterIndex] = state.split(":").map(Number);
|
|
359
|
+
if (characterIndex >= 0 && characterIndex < expected[tokenIndex].length) {
|
|
360
|
+
next.add(`${tokenIndex}:${characterIndex + 1}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const boundary = crossTokenBoundary(states);
|
|
364
|
+
if (boundary.matched)
|
|
365
|
+
return true;
|
|
366
|
+
states = new Set([...next, ...boundary.states]);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (/\s|[;&|()`]/.test(atom)) {
|
|
370
|
+
const boundary = crossTokenBoundary(states);
|
|
371
|
+
if (boundary.matched)
|
|
372
|
+
return true;
|
|
373
|
+
states = boundary.states;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const next = new Set();
|
|
377
|
+
for (const state of states) {
|
|
378
|
+
const [tokenIndex, characterIndex] = state.split(":").map(Number);
|
|
379
|
+
const wanted = expected[tokenIndex]?.[characterIndex];
|
|
380
|
+
next.add(wanted?.toLowerCase() === atom.toLowerCase() ? `${tokenIndex}:${characterIndex + 1}` : `${tokenIndex}:-1`);
|
|
381
|
+
}
|
|
382
|
+
states = next;
|
|
383
|
+
}
|
|
384
|
+
return crossTokenBoundary(states).matched;
|
|
385
|
+
}
|
|
386
|
+
const SHELL_COMMAND_BOUNDARIES = new Set(["||", "&&", ";;", "|&", "&", ";", "(", ")", "|", "<("]);
|
|
387
|
+
const SHELL_INTERPRETERS = new Set(["sh", "bash", "zsh"]);
|
|
388
|
+
const SHELL_TOKEN_BOUNDARIES = new Set(["{", "}", "then", "elif", "else", "do"]);
|
|
389
|
+
const SHELL_GRAMMAR_PREFIXES = new Set(["!", "if", "until", "while"]);
|
|
390
|
+
const SHELL_PARAMETER_MARKER = "__OURO_SHELL_PARAMETER_";
|
|
391
|
+
const SHELL_ARRAY_EXPANSION = "__OURO_ARRAY_EXPANSION__";
|
|
392
|
+
function hasUnescapedShellSubstitution(value) {
|
|
393
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
394
|
+
if (value[index] === "\\") {
|
|
395
|
+
index += 1;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (value.startsWith("$(", index) && !value.startsWith("$((", index))
|
|
399
|
+
return true;
|
|
400
|
+
if (value[index] === "`")
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
function stripHereDocBodies(command) {
|
|
406
|
+
const pending = [];
|
|
407
|
+
const lines = command.split("\n");
|
|
408
|
+
return lines.map((sourceLine) => {
|
|
409
|
+
if (pending.length > 0) {
|
|
410
|
+
const active = pending[0];
|
|
411
|
+
const candidate = active.stripTabs ? sourceLine.replace(/^\t+/, "") : sourceLine;
|
|
412
|
+
if (candidate === active.delimiter) {
|
|
413
|
+
pending.shift();
|
|
414
|
+
return "";
|
|
415
|
+
}
|
|
416
|
+
if (active.quoted || !hasUnescapedShellSubstitution(sourceLine))
|
|
417
|
+
return "";
|
|
418
|
+
return exposeShellSubstitutions(sourceLine).split("\n").slice(1).join("\n");
|
|
419
|
+
}
|
|
420
|
+
const pattern = /<<(?!<)(-)?\s*(?:'([^']*)'|"([^"]*)"|\\([^\s;&|]+)|([^\s;&|]+))/g;
|
|
421
|
+
for (const match of stripShellComments(sourceLine).matchAll(pattern)) {
|
|
422
|
+
pending.push({
|
|
423
|
+
stripTabs: match[1] !== undefined,
|
|
424
|
+
delimiter: match[2] ?? match[3] ?? match[4] ?? match[5],
|
|
425
|
+
quoted: match[2] !== undefined || match[3] !== undefined || match[4] !== undefined,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
return sourceLine;
|
|
429
|
+
}).join("\n");
|
|
430
|
+
}
|
|
431
|
+
function stripShellComments(command) {
|
|
432
|
+
let result = "";
|
|
433
|
+
let quote = null;
|
|
434
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
435
|
+
const character = command[index];
|
|
436
|
+
if (character === "\\" && quote !== "single") {
|
|
437
|
+
result += character + (command[index + 1] ?? "");
|
|
438
|
+
index += 1;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (character === "'" && quote !== "double")
|
|
442
|
+
quote = quote === "single" ? null : "single";
|
|
443
|
+
else if (character === '"' && quote !== "single")
|
|
444
|
+
quote = quote === "double" ? null : "double";
|
|
445
|
+
if (character === "#" && quote === null && (index === 0 || /[\s;&|()]/.test(command[index - 1]))) {
|
|
446
|
+
const newline = command.indexOf("\n", index);
|
|
447
|
+
if (newline < 0)
|
|
448
|
+
break;
|
|
449
|
+
result += " ".repeat(newline - index) + "\n";
|
|
450
|
+
index = newline;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
result += character;
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
const SHELL_ARRAY_ASSIGNMENT = /([A-Za-z_][A-Za-z0-9_]*)(\+?=)\(([^()\n]*)\)/y;
|
|
458
|
+
const SHELL_INDEXED_ASSIGNMENT = /([A-Za-z_][A-Za-z0-9_]*)\[(\d+)\]=("[^"]*"|'[^']*'|[^\s;&|]+)/y;
|
|
459
|
+
const SHELL_SCALAR_ASSIGNMENT = /([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|'[^']*'|[^\s;&|()]+)/y;
|
|
460
|
+
const SHELL_ARRAY_PARAMETER = /\$\{([A-Za-z_][A-Za-z0-9_]*)\[(@|\*)\]\}/y;
|
|
461
|
+
const SHELL_SCALAR_PARAMETER = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/y;
|
|
462
|
+
function shellMatchAt(pattern, command, index) {
|
|
463
|
+
pattern.lastIndex = index;
|
|
464
|
+
return pattern.exec(command);
|
|
465
|
+
}
|
|
466
|
+
function shellAssignmentPosition(command, index) {
|
|
467
|
+
let previous = index - 1;
|
|
468
|
+
while (previous >= 0 && /[ \t]/.test(command[previous]))
|
|
469
|
+
previous -= 1;
|
|
470
|
+
return previous < 0 || /[;&|()\n]/.test(command[previous]);
|
|
471
|
+
}
|
|
472
|
+
function parseShellWords(value) {
|
|
473
|
+
if (hasUnescapedShellSubstitution(value) || /(^|[^\\])\$(?!['"])/.test(value))
|
|
474
|
+
return undefined;
|
|
475
|
+
try {
|
|
476
|
+
const entries = (0, shell_quote_1.parse)(value);
|
|
477
|
+
if (entries.some((entry) => typeof entry !== "string"))
|
|
478
|
+
return undefined;
|
|
479
|
+
return entries;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function shellAssignmentPersists(command, endIndex) {
|
|
486
|
+
let next = endIndex;
|
|
487
|
+
while (next < command.length && /[ \t]/.test(command[next]))
|
|
488
|
+
next += 1;
|
|
489
|
+
return next === command.length || /[;&|\n]/.test(command[next]);
|
|
490
|
+
}
|
|
491
|
+
function shellUnquotedFields(values) {
|
|
492
|
+
const fields = values.flatMap((value) => value.split(/\s+/).filter(Boolean));
|
|
493
|
+
return fields.some((field) => /[*?[]/.test(field)) ? SHELL_ARRAY_EXPANSION : (0, shell_quote_1.quote)(fields);
|
|
494
|
+
}
|
|
495
|
+
function doubleQuotedFragment(value) {
|
|
496
|
+
if (hasUnescapedShellSubstitution(value))
|
|
497
|
+
return undefined;
|
|
498
|
+
try {
|
|
499
|
+
const parsed = (0, shell_quote_1.parse)(`"${value}"`);
|
|
500
|
+
return parsed.length === 1 && typeof parsed[0] === "string" ? parsed[0] : undefined;
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function closingDoubleQuote(command, startIndex) {
|
|
507
|
+
for (let index = startIndex; index < command.length; index += 1) {
|
|
508
|
+
if (command[index] === "\\")
|
|
509
|
+
index += 1;
|
|
510
|
+
else if (command[index] === '"')
|
|
511
|
+
return index;
|
|
512
|
+
}
|
|
513
|
+
return -1;
|
|
514
|
+
}
|
|
515
|
+
function transformOrderedShellVariables(command) {
|
|
516
|
+
const variables = new Map();
|
|
517
|
+
const result = [];
|
|
518
|
+
let quote = null;
|
|
519
|
+
let doubleQuoteStart = -1;
|
|
520
|
+
let doubleQuoteResultIndex = -1;
|
|
521
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
522
|
+
const character = command[index];
|
|
523
|
+
if (character === "\\" && quote !== "single") {
|
|
524
|
+
result.push(character + (command[index + 1] ?? ""));
|
|
525
|
+
index += 1;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (character === "'") {
|
|
529
|
+
if (quote !== "double")
|
|
530
|
+
quote = quote === "single" ? null : "single";
|
|
531
|
+
result.push(character);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (character === '"') {
|
|
535
|
+
if (quote !== "single") {
|
|
536
|
+
if (quote === "double") {
|
|
537
|
+
quote = null;
|
|
538
|
+
doubleQuoteStart = -1;
|
|
539
|
+
doubleQuoteResultIndex = -1;
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
quote = "double";
|
|
543
|
+
doubleQuoteStart = index;
|
|
544
|
+
doubleQuoteResultIndex = result.length;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
result.push(character);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (quote === null && /[A-Za-z_]/.test(character) && shellAssignmentPosition(command, index)) {
|
|
551
|
+
const arrayAssignment = shellMatchAt(SHELL_ARRAY_ASSIGNMENT, command, index);
|
|
552
|
+
if (arrayAssignment) {
|
|
553
|
+
const [, name, operator, body] = arrayAssignment;
|
|
554
|
+
const elements = parseShellWords(body);
|
|
555
|
+
if (elements === undefined)
|
|
556
|
+
variables.delete(name);
|
|
557
|
+
else
|
|
558
|
+
variables.set(name, { array: operator === "+=" ? [...(variables.get(name)?.array ?? []), ...elements] : elements });
|
|
559
|
+
result.push(`${name}=__OURO_ARRAY_LITERAL`);
|
|
560
|
+
index += arrayAssignment[0].length - 1;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const indexedAssignment = shellMatchAt(SHELL_INDEXED_ASSIGNMENT, command, index);
|
|
564
|
+
if (indexedAssignment) {
|
|
565
|
+
const [, name, elementIndex, rawValue] = indexedAssignment;
|
|
566
|
+
const element = parseShellWords(rawValue);
|
|
567
|
+
if (element?.length !== 1)
|
|
568
|
+
variables.delete(name);
|
|
569
|
+
else {
|
|
570
|
+
const array = [...(variables.get(name)?.array ?? [])];
|
|
571
|
+
array[Number(elementIndex)] = element[0];
|
|
572
|
+
variables.set(name, { array });
|
|
573
|
+
}
|
|
574
|
+
result.push(`${name}=__OURO_ARRAY_LITERAL`);
|
|
575
|
+
index += indexedAssignment[0].length - 1;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const scalarAssignment = shellMatchAt(SHELL_SCALAR_ASSIGNMENT, command, index);
|
|
579
|
+
if (scalarAssignment) {
|
|
580
|
+
const [, name, rawValue] = scalarAssignment;
|
|
581
|
+
const scalar = parseShellWords(rawValue);
|
|
582
|
+
if (shellAssignmentPersists(command, index + scalarAssignment[0].length)) {
|
|
583
|
+
if (scalar?.length === 1)
|
|
584
|
+
variables.set(name, { scalar: scalar[0] });
|
|
585
|
+
else
|
|
586
|
+
variables.delete(name);
|
|
587
|
+
}
|
|
588
|
+
result.push(scalarAssignment[0]);
|
|
589
|
+
index += scalarAssignment[0].length - 1;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (quote !== "single" && character === "$") {
|
|
594
|
+
const arrayExpansion = shellMatchAt(SHELL_ARRAY_PARAMETER, command, index);
|
|
595
|
+
if (arrayExpansion) {
|
|
596
|
+
const [, name, mode] = arrayExpansion;
|
|
597
|
+
const elements = variables.get(name)?.array;
|
|
598
|
+
if (elements === undefined)
|
|
599
|
+
result.push(SHELL_ARRAY_EXPANSION);
|
|
600
|
+
else if (quote === "double" && mode === "@") {
|
|
601
|
+
const closing = closingDoubleQuote(command, index + arrayExpansion[0].length);
|
|
602
|
+
const prefix = doubleQuotedFragment(command.slice(doubleQuoteStart + 1, index));
|
|
603
|
+
const suffix = closing < 0 ? undefined : doubleQuotedFragment(command.slice(index + arrayExpansion[0].length, closing));
|
|
604
|
+
if (closing < 0 || prefix === undefined || suffix === undefined)
|
|
605
|
+
result.push(SHELL_ARRAY_EXPANSION);
|
|
606
|
+
else {
|
|
607
|
+
const presentElements = elements.filter((element) => element !== undefined);
|
|
608
|
+
const expanded = presentElements.length === 0
|
|
609
|
+
? (prefix.length + suffix.length === 0 ? [] : [`${prefix}${suffix}`])
|
|
610
|
+
: presentElements.map((element, elementIndex) => `${elementIndex === 0 ? prefix : ""}${element}${elementIndex === presentElements.length - 1 ? suffix : ""}`);
|
|
611
|
+
result.splice(doubleQuoteResultIndex);
|
|
612
|
+
result.push((0, shell_quote_1.quote)(expanded));
|
|
613
|
+
quote = null;
|
|
614
|
+
doubleQuoteStart = -1;
|
|
615
|
+
doubleQuoteResultIndex = -1;
|
|
616
|
+
index = closing;
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
else if (quote === "double")
|
|
621
|
+
result.push(elements.join(" "));
|
|
622
|
+
else
|
|
623
|
+
result.push(shellUnquotedFields(elements));
|
|
624
|
+
index += arrayExpansion[0].length - 1;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const scalarExpansion = shellMatchAt(SHELL_SCALAR_PARAMETER, command, index);
|
|
628
|
+
if (scalarExpansion) {
|
|
629
|
+
const name = scalarExpansion[1] ?? scalarExpansion[2];
|
|
630
|
+
const scalar = variables.get(name)?.scalar;
|
|
631
|
+
if (scalar !== undefined) {
|
|
632
|
+
if (quote === "double")
|
|
633
|
+
result.push(scalar.replace(/["\\$`]/g, "\\$&"));
|
|
634
|
+
else
|
|
635
|
+
result.push(shellUnquotedFields([scalar]));
|
|
636
|
+
index += scalarExpansion[0].length - 1;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (quote === null) {
|
|
640
|
+
result.push(SHELL_ARRAY_EXPANSION);
|
|
641
|
+
index += scalarExpansion[0].length - 1;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
result.push(character);
|
|
647
|
+
}
|
|
648
|
+
return result.join("");
|
|
649
|
+
}
|
|
650
|
+
function findCommandSubstitutionEnd(command, start) {
|
|
651
|
+
let depth = 1;
|
|
652
|
+
let quote = null;
|
|
653
|
+
for (let index = start; index < command.length; index += 1) {
|
|
654
|
+
const character = command[index];
|
|
655
|
+
if (character === "\\" && quote !== "single") {
|
|
656
|
+
index += 1;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (character === "'" && quote !== "double") {
|
|
660
|
+
quote = quote === "single" ? null : "single";
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (character === '"' && quote !== "single") {
|
|
664
|
+
quote = quote === "double" ? null : "double";
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
if (quote !== null)
|
|
668
|
+
continue;
|
|
669
|
+
if (character === "(")
|
|
670
|
+
depth += 1;
|
|
671
|
+
else if (character === ")") {
|
|
672
|
+
depth -= 1;
|
|
673
|
+
if (depth === 0)
|
|
674
|
+
return index;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return -1;
|
|
678
|
+
}
|
|
679
|
+
function exposeShellSubstitutions(command, depth = 0) {
|
|
680
|
+
if (depth >= 32)
|
|
681
|
+
return "ouro mail sync-cache";
|
|
682
|
+
let outer = "";
|
|
683
|
+
const nested = [];
|
|
684
|
+
let quote = null;
|
|
685
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
686
|
+
const character = command[index];
|
|
687
|
+
if (character === "\\" && quote !== "single") {
|
|
688
|
+
outer += character + (command[index + 1] ?? "");
|
|
689
|
+
index += 1;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (character === "'" && quote !== "double") {
|
|
693
|
+
quote = quote === "single" ? null : "single";
|
|
694
|
+
outer += character;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (character === '"' && quote !== "single") {
|
|
698
|
+
quote = quote === "double" ? null : "double";
|
|
699
|
+
outer += character;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (quote !== "single" && command.startsWith("$(", index) && !command.startsWith("$((", index)) {
|
|
703
|
+
const end = findCommandSubstitutionEnd(command, index + 2);
|
|
704
|
+
if (end >= 0) {
|
|
705
|
+
nested.push(exposeShellSubstitutions(command.slice(index + 2, end), depth + 1));
|
|
706
|
+
outer += "${__OURO_COMMAND_SUBSTITUTION}";
|
|
707
|
+
index = end;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (quote !== "single" && character === "`") {
|
|
712
|
+
let end = index + 1;
|
|
713
|
+
while (end < command.length && command[end] !== "`") {
|
|
714
|
+
if (command[end] === "\\")
|
|
715
|
+
end += 1;
|
|
716
|
+
end += 1;
|
|
717
|
+
}
|
|
718
|
+
if (end < command.length) {
|
|
719
|
+
nested.push(exposeShellSubstitutions(command.slice(index + 1, end), depth + 1));
|
|
720
|
+
outer += "${__OURO_COMMAND_SUBSTITUTION}";
|
|
721
|
+
index = end;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
outer += character;
|
|
726
|
+
}
|
|
727
|
+
return [outer, ...nested].join("\n");
|
|
728
|
+
}
|
|
729
|
+
function normalizeShellNewlines(command) {
|
|
730
|
+
let result = "";
|
|
731
|
+
let quote = null;
|
|
732
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
733
|
+
const character = command[index];
|
|
734
|
+
if (character === "\\" && quote !== "single") {
|
|
735
|
+
result += character + (command[index + 1] ?? "");
|
|
736
|
+
index += 1;
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
if (character === "'" && quote !== "double")
|
|
740
|
+
quote = quote === "single" ? null : "single";
|
|
741
|
+
else if (character === '"' && quote !== "single")
|
|
742
|
+
quote = quote === "double" ? null : "double";
|
|
743
|
+
result += character === "\n" && quote === null ? " ; " : character;
|
|
744
|
+
}
|
|
745
|
+
return result;
|
|
746
|
+
}
|
|
747
|
+
function executableShellView(command) {
|
|
748
|
+
const withoutData = stripShellComments(stripHereDocBodies(command));
|
|
749
|
+
return normalizeShellNewlines(transformOrderedShellVariables(exposeShellSubstitutions(withoutData)));
|
|
750
|
+
}
|
|
751
|
+
function parseShellCommands(command) {
|
|
752
|
+
let parameterIndex = 0;
|
|
753
|
+
const parameterNames = new Map();
|
|
754
|
+
const entries = (0, shell_quote_1.parse)(command, (name) => {
|
|
755
|
+
const marker = `${SHELL_PARAMETER_MARKER}${parameterIndex}__`;
|
|
756
|
+
parameterIndex += 1;
|
|
757
|
+
parameterNames.set(marker, name);
|
|
758
|
+
return marker;
|
|
759
|
+
});
|
|
760
|
+
const commands = [];
|
|
761
|
+
let current = [];
|
|
762
|
+
let skipRedirectionTarget = false;
|
|
763
|
+
const flush = () => {
|
|
764
|
+
if (current.length > 0)
|
|
765
|
+
commands.push(current);
|
|
766
|
+
current = [];
|
|
767
|
+
skipRedirectionTarget = false;
|
|
768
|
+
};
|
|
769
|
+
for (const entry of entries) {
|
|
770
|
+
if (typeof entry === "string") {
|
|
771
|
+
if (skipRedirectionTarget)
|
|
772
|
+
skipRedirectionTarget = false;
|
|
773
|
+
else if (SHELL_TOKEN_BOUNDARIES.has(entry.toLowerCase()))
|
|
774
|
+
flush();
|
|
775
|
+
else
|
|
776
|
+
current.push(entry);
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
const operation = entry;
|
|
780
|
+
if (operation.op === "glob") {
|
|
781
|
+
current.push(operation.pattern);
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (SHELL_COMMAND_BOUNDARIES.has(operation.op)) {
|
|
785
|
+
flush();
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
// Every remaining control operator is a redirection. Its next string is
|
|
789
|
+
// a path or descriptor, not an argv token.
|
|
790
|
+
skipRedirectionTarget = true;
|
|
791
|
+
}
|
|
792
|
+
flush();
|
|
793
|
+
return { commands, parameterNames };
|
|
794
|
+
}
|
|
795
|
+
function resolveKnownShellParameters(token, knownVariables, parameterNames) {
|
|
796
|
+
let resolved = token;
|
|
797
|
+
for (const [marker, name] of parameterNames) {
|
|
798
|
+
const known = knownVariables.get(name);
|
|
799
|
+
if (known !== undefined)
|
|
800
|
+
resolved = resolved.replaceAll(marker, known);
|
|
801
|
+
}
|
|
802
|
+
return resolved;
|
|
803
|
+
}
|
|
804
|
+
function shellTokenCanEqual(token, expected) {
|
|
805
|
+
const escaped = token
|
|
806
|
+
.split(new RegExp(`(?:${SHELL_PARAMETER_MARKER}\\d+__|${SHELL_ARRAY_EXPANSION})`, "g"))
|
|
807
|
+
.map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
808
|
+
.join(".*");
|
|
809
|
+
return new RegExp(`^${escaped}$`, "i").test(expected);
|
|
810
|
+
}
|
|
811
|
+
function shellExecutableCanEqual(token, expected) {
|
|
812
|
+
const basenameToken = token.slice(token.lastIndexOf("/") + 1);
|
|
813
|
+
return shellTokenCanEqual(basenameToken, expected);
|
|
814
|
+
}
|
|
815
|
+
function exactShellExecutable(token) {
|
|
816
|
+
if (token === undefined || token.includes(SHELL_PARAMETER_MARKER))
|
|
817
|
+
return undefined;
|
|
818
|
+
return token.slice(token.lastIndexOf("/") + 1).toLowerCase();
|
|
819
|
+
}
|
|
820
|
+
const SUDO_SHORT_OPERANDS = new Set(["-C", "-D", "-g", "-h", "-p", "-R", "-r", "-T", "-t", "-U", "-u"]);
|
|
821
|
+
const SUDO_LONG_OPERANDS = new Set(["--chdir", "--chroot", "--close-from", "--command-timeout", "--group", "--host", "--other-user", "--prompt", "--role", "--type", "--user"]);
|
|
822
|
+
const TIME_SHORT_OPERANDS = new Set(["-f", "-o"]);
|
|
823
|
+
const TIME_LONG_OPERANDS = new Set(["--format", "--output"]);
|
|
824
|
+
function wrapperOptionAdvance(option, shortOperands, longOperands) {
|
|
825
|
+
if (shortOperands.has(option) || longOperands.has(option))
|
|
826
|
+
return 2;
|
|
827
|
+
if ([...shortOperands].some((candidate) => option.startsWith(candidate) && option.length > candidate.length))
|
|
828
|
+
return 1;
|
|
829
|
+
if ([...longOperands].some((candidate) => option.startsWith(`${candidate}=`)))
|
|
830
|
+
return 1;
|
|
831
|
+
return 0;
|
|
832
|
+
}
|
|
833
|
+
function resolveShellCommand(tokens, startIndex) {
|
|
834
|
+
let index = startIndex;
|
|
835
|
+
while (index < tokens.length) {
|
|
836
|
+
const executable = exactShellExecutable(tokens[index]);
|
|
837
|
+
if (SHELL_GRAMMAR_PREFIXES.has(executable ?? "")) {
|
|
838
|
+
index += 1;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (executable === "coproc") {
|
|
842
|
+
index += 1;
|
|
843
|
+
const candidate = tokens[index] ?? "";
|
|
844
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(candidate) && !shellExecutableCanEqual(candidate, "ouro") && tokens[index + 1] !== undefined)
|
|
845
|
+
index += 1;
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if (executable === "command" || executable === "nohup") {
|
|
849
|
+
index += 1;
|
|
850
|
+
if (executable === "command" && (tokens[index] === "-v" || tokens[index] === "-V"))
|
|
851
|
+
return { index: tokens.length };
|
|
852
|
+
while (tokens[index]?.startsWith("-"))
|
|
853
|
+
index += 1;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (executable === "env") {
|
|
857
|
+
index += 1;
|
|
858
|
+
while (index < tokens.length) {
|
|
859
|
+
const option = tokens[index];
|
|
860
|
+
if (option === "-S" || option === "--split-string")
|
|
861
|
+
return { index, script: `env ${tokens.slice(index + 1).join(" ")}` };
|
|
862
|
+
if (option.startsWith("--split-string="))
|
|
863
|
+
return { index, script: `env ${[option.slice("--split-string=".length), ...tokens.slice(index + 1)].join(" ")}` };
|
|
864
|
+
if (["-u", "-C", "--unset", "--chdir"].includes(option))
|
|
865
|
+
index += 2;
|
|
866
|
+
else if (option.startsWith("-") || /^[A-Za-z_][A-Za-z0-9_]*=/.test(option))
|
|
867
|
+
index += 1;
|
|
868
|
+
else
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (executable === "sudo") {
|
|
874
|
+
index += 1;
|
|
875
|
+
while (tokens[index]?.startsWith("-")) {
|
|
876
|
+
const advance = wrapperOptionAdvance(tokens[index], SUDO_SHORT_OPERANDS, SUDO_LONG_OPERANDS);
|
|
877
|
+
index += advance === 0 ? 1 : advance;
|
|
878
|
+
}
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
if (executable === "time") {
|
|
882
|
+
index += 1;
|
|
883
|
+
while (tokens[index]?.startsWith("-")) {
|
|
884
|
+
const advance = wrapperOptionAdvance(tokens[index], TIME_SHORT_OPERANDS, TIME_LONG_OPERANDS);
|
|
885
|
+
index += advance === 0 ? 1 : advance;
|
|
886
|
+
}
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
if (executable === "exec") {
|
|
890
|
+
index += 1;
|
|
891
|
+
while (tokens[index]?.startsWith("-"))
|
|
892
|
+
index += tokens[index] === "-a" ? 2 : 1;
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
return { index };
|
|
898
|
+
}
|
|
899
|
+
function shellScriptRequiresFamily(script, variables, depth) {
|
|
900
|
+
if (script.includes(SHELL_PARAMETER_MARKER) || script.includes(SHELL_ARRAY_EXPANSION))
|
|
901
|
+
return true;
|
|
902
|
+
return shellCommandsContainMailCacheSync(script, variables, depth + 1);
|
|
903
|
+
}
|
|
904
|
+
function shellTokensContainMailCacheSync(tokens, startIndex, variables, depth) {
|
|
905
|
+
const resolved = resolveShellCommand(tokens, startIndex);
|
|
906
|
+
if (resolved.script !== undefined)
|
|
907
|
+
return shellScriptRequiresFamily(resolved.script, variables, depth);
|
|
908
|
+
const executableToken = tokens[resolved.index] ?? "";
|
|
909
|
+
const executable = exactShellExecutable(executableToken);
|
|
910
|
+
if (executableToken.includes(SHELL_ARRAY_EXPANSION))
|
|
911
|
+
return true;
|
|
912
|
+
if (SHELL_INTERPRETERS.has(executable ?? "")) {
|
|
913
|
+
const commandOption = tokens.findIndex((token, index) => index > resolved.index && /^-[^-]*c/.test(token));
|
|
914
|
+
return commandOption >= 0
|
|
915
|
+
&& shellScriptRequiresFamily(tokens[commandOption + 1] ?? "", variables, depth);
|
|
916
|
+
}
|
|
917
|
+
if (executable === "eval")
|
|
918
|
+
return shellScriptRequiresFamily(tokens.slice(resolved.index + 1).join(" "), variables, depth);
|
|
919
|
+
return shellExecutableCanEqual(executableToken, "ouro")
|
|
920
|
+
&& shellTokenCanEqual(tokens[resolved.index + 1] ?? "", "mail")
|
|
921
|
+
&& shellTokenCanEqual(tokens[resolved.index + 2] ?? "", "sync-cache");
|
|
922
|
+
}
|
|
923
|
+
function shellCommandsContainMailCacheSync(command, inheritedVariables = new Map(), depth = 0) {
|
|
924
|
+
if (depth >= 32)
|
|
925
|
+
return true;
|
|
926
|
+
let parsed;
|
|
927
|
+
try {
|
|
928
|
+
parsed = parseShellCommands(command);
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
const knownVariables = new Map(inheritedVariables);
|
|
934
|
+
for (const rawTokens of parsed.commands) {
|
|
935
|
+
const tokens = rawTokens.map((token) => resolveKnownShellParameters(token, knownVariables, parsed.parameterNames));
|
|
936
|
+
const invocationVariables = new Map(knownVariables);
|
|
937
|
+
let commandIndex = 0;
|
|
938
|
+
while (commandIndex < tokens.length) {
|
|
939
|
+
const assignment = tokens[commandIndex].match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s);
|
|
940
|
+
if (!assignment)
|
|
941
|
+
break;
|
|
942
|
+
if (assignment[2].includes(SHELL_PARAMETER_MARKER))
|
|
943
|
+
invocationVariables.delete(assignment[1]);
|
|
944
|
+
else
|
|
945
|
+
invocationVariables.set(assignment[1], assignment[2]);
|
|
946
|
+
commandIndex += 1;
|
|
947
|
+
}
|
|
948
|
+
if (commandIndex === tokens.length) {
|
|
949
|
+
knownVariables.clear();
|
|
950
|
+
for (const [name, value] of invocationVariables)
|
|
951
|
+
knownVariables.set(name, value);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (shellTokensContainMailCacheSync(tokens, commandIndex, invocationVariables, depth))
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
function shellContainsMailCacheSync(command) {
|
|
960
|
+
const executableCommand = executableShellView(command);
|
|
961
|
+
if (shellContainsAmbiguousAnsiMailCacheSync(executableCommand))
|
|
962
|
+
return true;
|
|
963
|
+
return shellCommandsContainMailCacheSync(executableCommand);
|
|
964
|
+
}
|
|
965
|
+
function mailCacheSyncShellGuardrail(toolName, args, context) {
|
|
966
|
+
if (toolName !== "shell" || !shellContainsMailCacheSync(args.command ?? ""))
|
|
967
|
+
return allow;
|
|
968
|
+
if (context.trustLevel === undefined || context.trustLevel === "family")
|
|
969
|
+
return allow;
|
|
970
|
+
return deny("hosted mail cache convergence requires family trust.");
|
|
971
|
+
}
|
|
301
972
|
function mailTrustGuardrail(toolName, args, context) {
|
|
302
973
|
if (MAIL_FAMILY_TOOLS.has(toolName)) {
|
|
303
974
|
if (context.trustLevel === undefined || context.trustLevel === "family")
|
|
@@ -360,6 +1031,9 @@ function checkFirstClassMcpTrust(context) {
|
|
|
360
1031
|
return allow;
|
|
361
1032
|
}
|
|
362
1033
|
function checkTrustLevelGuardrails(toolName, args, context) {
|
|
1034
|
+
const cacheSyncResult = mailCacheSyncShellGuardrail(toolName, args, context);
|
|
1035
|
+
if (!cacheSyncResult.allowed)
|
|
1036
|
+
return cacheSyncResult;
|
|
363
1037
|
const mailResult = mailTrustGuardrail(toolName, args, context);
|
|
364
1038
|
if (!mailResult.allowed)
|
|
365
1039
|
return mailResult;
|