@lazyingart/agintiflow 0.20.218 → 0.20.219

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.218",
3
+ "version": "0.20.219",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
@@ -635,6 +635,48 @@ try {
635
635
  `unsafe or dynamic shell loop bypassed the bounded read-only policy: ${unsafeLoop}`
636
636
  );
637
637
  }
638
+ const exactCompoundReadOnlyAudit = [
639
+ 'echo "=== CWD ==="',
640
+ 'pwd',
641
+ 'echo "=== workspace listing ==="',
642
+ 'ls -la',
643
+ 'echo "=== workspace git status ==="',
644
+ 'git status --short 2>&1 | head -40',
645
+ 'echo "=== read roots ==="',
646
+ 'for d in /home/lachlan/ProjectsLFS/AgenticApp /home/lachlan/ProjectsLFS/Musia /home/lachlan/ProjectsLFS/LALACHAN /home/lachlan/DiskMech/Projects/lazyedit; do echo "--- $d ---"; if [ -d "$d" ]; then ls -la "$d" 2>&1 | head -80; else echo "MISSING"; fi; done',
647
+ ].join("; ");
648
+ const exactCompoundReadOnlyAuditPolicy = evaluateCommandPolicy(
649
+ exactCompoundReadOnlyAudit,
650
+ hostReadRootPolicy
651
+ );
652
+ assert(
653
+ exactCompoundReadOnlyAuditPolicy.allowed &&
654
+ exactCompoundReadOnlyAuditPolicy.category === "read-only" &&
655
+ exactCompoundReadOnlyAuditPolicy.boundedForLoop === true &&
656
+ exactCompoundReadOnlyAuditPolicy.boundedCompoundSequence === true &&
657
+ exactCompoundReadOnlyAuditPolicy.needsNetwork === false &&
658
+ exactCompoundReadOnlyAuditPolicy.writesWorkspace === false,
659
+ "a finite read-only prelude and conditional directory audit incorrectly required destructive host permission"
660
+ );
661
+ for (const unsafeCompoundAudit of [
662
+ 'cp README.md copied.md; for d in /tmp; do echo "$d"; done',
663
+ 'echo start; for d in /tmp; do if [ -d "$d" ]; then rm -rf "$d"; else echo missing; fi; done',
664
+ 'echo start; for d in /tmp; do if [ -d "$d" ]; then curl https://example.com; else echo missing; fi; done',
665
+ 'echo start; for d in /tmp; do if [ -d "$d" ]; then echo "$d" > report.md; else echo missing; fi; done',
666
+ 'echo start; for d in /tmp; do if [ -d "$(pwd)" ]; then ls; else echo missing; fi; done',
667
+ 'echo start; for d in /tmp; do if [ -d "$d" ]; then if [ -f "$d/x" ]; then cat "$d/x"; else echo missing; fi; else echo absent; fi; done',
668
+ 'echo start; for d in $ROOTS; do if [ -d "$d" ]; then ls "$d"; else echo missing; fi; done',
669
+ 'echo start; for d in /tmp; do if [ -d "$d" ]; then ls "$d"; else echo missing; fi; done; rm -rf report.md',
670
+ ]) {
671
+ const unsafeCompoundAuditPolicy = evaluateCommandPolicy(unsafeCompoundAudit, hostReadRootPolicy);
672
+ assert(
673
+ !unsafeCompoundAuditPolicy.allowed ||
674
+ unsafeCompoundAuditPolicy.category !== "read-only" ||
675
+ unsafeCompoundAuditPolicy.writesWorkspace === true ||
676
+ unsafeCompoundAuditPolicy.needsNetwork === true,
677
+ `unsafe compound shell audit bypassed the bounded read-only policy: ${unsafeCompoundAudit}`
678
+ );
679
+ }
638
680
  const readRootDoctorPolicy = evaluateCommandPolicy(
639
681
  `cd ${externalReadRoot} && node bin/musia.js doctor --json 2>&1 | head -80`,
640
682
  hostReadRootPolicy
@@ -16,7 +16,7 @@ const READ_ONLY_PATTERNS = [
16
16
  /^which\s+[-\w.]+(?:\s+[-\w.]+)*$/,
17
17
  /^command\s+-v\s+[-\w.]+$/,
18
18
  /^uname(?:\s+-a)?$/,
19
- /^ls(?:\s+[-\w./~*]+)*$/,
19
+ /^ls(?:\s+(?:"[^"\n]*"|'[^'\n]*'|[-\w./~*]+))*$/,
20
20
  /^find(?:\s+[./~\w-]+)*(?:\s+-maxdepth\s+\d+)?(?:\s+-type\s+[fd])?$/,
21
21
  /^rg(?:\s+.+)?$/,
22
22
  /^grep(?:\s+.+)?$/,
@@ -1389,6 +1389,135 @@ function broadForLoopClassification(normalized, reason) {
1389
1389
  };
1390
1390
  }
1391
1391
 
1392
+ function findUnquotedShellWord(value = "", expectedWord = "", startIndex = 0) {
1393
+ const text = String(value || "");
1394
+ const word = String(expectedWord || "");
1395
+ let quote = "";
1396
+ let escaped = false;
1397
+
1398
+ for (let index = 0; index <= text.length - word.length; index += 1) {
1399
+ const char = text[index];
1400
+ if (quote === "'") {
1401
+ if (char === "'") quote = "";
1402
+ continue;
1403
+ }
1404
+ if (escaped) {
1405
+ escaped = false;
1406
+ continue;
1407
+ }
1408
+ if (char === "\\") {
1409
+ escaped = true;
1410
+ continue;
1411
+ }
1412
+ if (quote === '"') {
1413
+ if (char === '"') quote = "";
1414
+ continue;
1415
+ }
1416
+ if (char === "'" || char === '"') {
1417
+ quote = char;
1418
+ continue;
1419
+ }
1420
+ if (index < Math.max(0, startIndex) || text.slice(index, index + word.length) !== word) continue;
1421
+ const before = index > 0 ? text[index - 1] : "";
1422
+ const after = text[index + word.length] || "";
1423
+ if ((!before || !/[A-Za-z0-9_]/.test(before)) && (!after || !/[A-Za-z0-9_]/.test(after))) {
1424
+ return index;
1425
+ }
1426
+ }
1427
+ return -1;
1428
+ }
1429
+
1430
+ function trimShellListBoundary(value = "") {
1431
+ return String(value || "")
1432
+ .replace(/^[\s;]+/, "")
1433
+ .replace(/[\s;]+$/, "")
1434
+ .trim();
1435
+ }
1436
+
1437
+ function classifyReadOnlyCommandList(value = "") {
1438
+ const text = trimShellListBoundary(value);
1439
+ if (!text) {
1440
+ return {
1441
+ category: "read-only",
1442
+ needsNetwork: false,
1443
+ writesWorkspace: false,
1444
+ emptyCommandList: true,
1445
+ };
1446
+ }
1447
+ const sequence = splitTopLevelShellSequence(text);
1448
+ if (sequence && sequence.parts.length > READ_ONLY_FOR_LOOP_MAX_COMMANDS) {
1449
+ return broadForLoopClassification(text, "the command list is too large");
1450
+ }
1451
+ return classifyShellSequence(text) || classifyPipelineSequence(text) || classifySimpleCommand(text);
1452
+ }
1453
+
1454
+ function isReadOnlyShellCondition(value = "") {
1455
+ const tokens = tokenizeShellWords(trimShellListBoundary(value));
1456
+ let option = "";
1457
+ let candidate = "";
1458
+ if (tokens.length === 4 && tokens[0] === "[" && tokens[3] === "]") {
1459
+ [, option, candidate] = tokens;
1460
+ } else if (tokens.length === 3 && tokens[0] === "test") {
1461
+ [, option, candidate] = tokens;
1462
+ } else {
1463
+ return false;
1464
+ }
1465
+ return /^-[efdx]$/.test(option) &&
1466
+ !candidate.startsWith("-") &&
1467
+ READ_ONLY_FOR_LOOP_LITERAL_PATTERN.test(candidate);
1468
+ }
1469
+
1470
+ function classifyReadOnlyLoopBody(bodySource = "") {
1471
+ const text = String(bodySource || "").trim();
1472
+ const controlWords = ["if", "then", "else", "elif", "fi", "for", "do", "done", "while", "until", "case", "esac"];
1473
+ const presentControls = controlWords.filter((word) => findUnquotedShellWord(text, word) >= 0);
1474
+ if (!presentControls.length) return classifyReadOnlyCommandList(text);
1475
+
1476
+ if (presentControls.some((word) => !["if", "then", "else", "fi"].includes(word))) {
1477
+ return broadForLoopClassification(text, "the body contains nested or unsupported control flow");
1478
+ }
1479
+
1480
+ const ifIndex = findUnquotedShellWord(text, "if");
1481
+ const thenIndex = findUnquotedShellWord(text, "then", ifIndex + 2);
1482
+ const elseIndex = findUnquotedShellWord(text, "else", thenIndex + 4);
1483
+ const fiIndex = findUnquotedShellWord(text, "fi", elseIndex + 4);
1484
+ if (ifIndex < 0 || thenIndex < 0 || elseIndex < 0 || fiIndex < 0 ||
1485
+ findUnquotedShellWord(text, "if", ifIndex + 2) >= 0 ||
1486
+ findUnquotedShellWord(text, "then", thenIndex + 4) >= 0 ||
1487
+ findUnquotedShellWord(text, "else", elseIndex + 4) >= 0 ||
1488
+ findUnquotedShellWord(text, "fi", fiIndex + 2) >= 0) {
1489
+ return broadForLoopClassification(text, "the conditional is not a single bounded if/else block");
1490
+ }
1491
+
1492
+ const prefix = trimShellListBoundary(text.slice(0, ifIndex));
1493
+ const condition = trimShellListBoundary(text.slice(ifIndex + 2, thenIndex));
1494
+ const thenBranch = trimShellListBoundary(text.slice(thenIndex + 4, elseIndex));
1495
+ const elseBranch = trimShellListBoundary(text.slice(elseIndex + 4, fiIndex));
1496
+ const suffix = trimShellListBoundary(text.slice(fiIndex + 2));
1497
+ if (!isReadOnlyShellCondition(condition) || !thenBranch || !elseBranch) {
1498
+ return broadForLoopClassification(text, "the conditional test or branch is not bounded read-only syntax");
1499
+ }
1500
+
1501
+ const classifications = [prefix, thenBranch, elseBranch, suffix]
1502
+ .filter(Boolean)
1503
+ .map((part) => classifyReadOnlyCommandList(part));
1504
+ const blocked = classifications.find(
1505
+ (classification) => classification.category === "blocked" || classification.category === "destructive"
1506
+ );
1507
+ if (blocked) return { ...blocked, gitOnly: false };
1508
+ if (classifications.some(
1509
+ (classification) => classification.category !== "read-only" || classification.writesWorkspace
1510
+ )) {
1511
+ return broadForLoopClassification(text, "a conditional branch is not read-only");
1512
+ }
1513
+ return {
1514
+ category: "read-only",
1515
+ needsNetwork: false,
1516
+ writesWorkspace: false,
1517
+ boundedConditional: true,
1518
+ };
1519
+ }
1520
+
1392
1521
  function classifyReadOnlyForLoop(normalized) {
1393
1522
  const text = String(normalized || "").trim();
1394
1523
  if (!/^for\s+/.test(text)) return null;
@@ -1416,9 +1545,8 @@ function classifyReadOnlyForLoop(normalized) {
1416
1545
  );
1417
1546
  }
1418
1547
 
1419
- const unquotedBody = stripQuotedSegments(bodySource);
1420
- if (/[|&<>(){}]/.test(unquotedBody) || hasActiveShellCommandSubstitution(bodySource)) {
1421
- return broadForLoopClassification(text, "the body uses control, redirection, or substitution syntax");
1548
+ if (hasActiveShellCommandSubstitution(bodySource)) {
1549
+ return broadForLoopClassification(text, "the body uses command substitution");
1422
1550
  }
1423
1551
 
1424
1552
  const escapedVariable = shellIdentifierPattern(variableName);
@@ -1436,23 +1564,12 @@ function classifyReadOnlyForLoop(normalized) {
1436
1564
  if (hasActiveShellExpansion(expandedBody) || hasActiveShellCommandSubstitution(expandedBody)) {
1437
1565
  return broadForLoopClassification(text, "the body contains expansion beyond the loop variable");
1438
1566
  }
1439
- const sequence = splitTopLevelShellSequence(expandedBody);
1440
- const commands = sequence?.parts || [expandedBody.trim()];
1441
- if (
1442
- !commands.length ||
1443
- commands.length > READ_ONLY_FOR_LOOP_MAX_COMMANDS ||
1444
- (sequence && sequence.separators.some((separator) => ![";", "newline"].includes(separator)))
1445
- ) {
1446
- return broadForLoopClassification(text, "the body is not a bounded sequential command list");
1567
+ const classification = classifyReadOnlyLoopBody(expandedBody);
1568
+ if (classification.category === "blocked" || classification.category === "destructive") {
1569
+ return { ...classification, gitOnly: false };
1447
1570
  }
1448
- for (const command of commands) {
1449
- const classification = classifySimpleCommand(command);
1450
- if (classification.category === "blocked" || classification.category === "destructive") {
1451
- return { ...classification, gitOnly: false };
1452
- }
1453
- if (classification.category !== "read-only" || classification.writesWorkspace) {
1454
- return broadForLoopClassification(text, `body command is ${classification.category}`);
1455
- }
1571
+ if (classification.category !== "read-only" || classification.writesWorkspace) {
1572
+ return broadForLoopClassification(text, `body is ${classification.category}`);
1456
1573
  }
1457
1574
  }
1458
1575
 
@@ -1466,6 +1583,46 @@ function classifyReadOnlyForLoop(normalized) {
1466
1583
  };
1467
1584
  }
1468
1585
 
1586
+ function classifyReadOnlyCompoundSequence(normalized) {
1587
+ const text = String(normalized || "").trim();
1588
+ if (!text || /^for\s+/.test(text) || hasActiveShellCommandSubstitution(text)) return null;
1589
+
1590
+ let forIndex = findUnquotedShellWord(text, "for");
1591
+ const startsAtCommandBoundary = (index) => {
1592
+ let boundary = index - 1;
1593
+ while (boundary >= 0 && /[ \t]/.test(text[boundary])) boundary -= 1;
1594
+ return boundary >= 0 && /[;\n\r]/.test(text[boundary]);
1595
+ };
1596
+ while (forIndex > 0 && !startsAtCommandBoundary(forIndex)) {
1597
+ forIndex = findUnquotedShellWord(text, "for", forIndex + 3);
1598
+ }
1599
+ if (forIndex <= 0) return null;
1600
+
1601
+ const prefixSource = trimShellListBoundary(text.slice(0, forIndex));
1602
+ const loopSource = text.slice(forIndex).trim();
1603
+ const prefixClassification = classifyReadOnlyCommandList(prefixSource);
1604
+ if (prefixClassification.category === "blocked" || prefixClassification.category === "destructive") {
1605
+ return { ...prefixClassification, gitOnly: false };
1606
+ }
1607
+ if (prefixClassification.category !== "read-only" || prefixClassification.writesWorkspace) {
1608
+ return broadForLoopClassification(text, "the command prelude is not read-only");
1609
+ }
1610
+ const loopClassification = classifyReadOnlyForLoop(loopSource);
1611
+ if (!loopClassification) return null;
1612
+ if (loopClassification.category !== "read-only" || loopClassification.writesWorkspace) {
1613
+ return loopClassification;
1614
+ }
1615
+ return {
1616
+ category: "read-only",
1617
+ needsNetwork: false,
1618
+ writesWorkspace: false,
1619
+ gitOnly: false,
1620
+ boundedForLoop: true,
1621
+ boundedCompoundSequence: true,
1622
+ reason: "Finite read-only command prelude followed by a bounded read-only shell loop.",
1623
+ };
1624
+ }
1625
+
1469
1626
  function splitTopLevelPipeline(command = "") {
1470
1627
  const parts = [];
1471
1628
  let current = "";
@@ -1630,7 +1787,7 @@ export function classifyCommand(command) {
1630
1787
  const normalized = String(command || "").trim();
1631
1788
  if (!normalized) return { category: "blocked", reason: "Command is empty." };
1632
1789
 
1633
- return classifyBackgroundShell(normalized) || classifyReadOnlyForLoop(normalized) || classifyCdCommand(normalized) || classifyShellSequence(normalized) || classifyPipelineSequence(normalized) || classifySimpleCommand(normalized);
1790
+ return classifyBackgroundShell(normalized) || classifyReadOnlyCompoundSequence(normalized) || classifyReadOnlyForLoop(normalized) || classifyCdCommand(normalized) || classifyShellSequence(normalized) || classifyPipelineSequence(normalized) || classifySimpleCommand(normalized);
1634
1791
  }
1635
1792
 
1636
1793
  export function evaluateCommandPolicy(command, config = {}) {