@lazyingart/agintiflow 0.20.219 → 0.20.221

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.219",
3
+ "version": "0.20.221",
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",
@@ -658,6 +658,77 @@ try {
658
658
  exactCompoundReadOnlyAuditPolicy.writesWorkspace === false,
659
659
  "a finite read-only prelude and conditional directory audit incorrectly required destructive host permission"
660
660
  );
661
+ const exactReadOnlyGitIdentityProbe =
662
+ 'git status --short && echo "TOPLEVEL=$(git rev-parse --show-toplevel 2>&1)" && echo "BRANCH=$(git rev-parse --abbrev-ref HEAD 2>&1)"';
663
+ const exactReadOnlyGitIdentityProbePolicy = evaluateCommandPolicy(
664
+ exactReadOnlyGitIdentityProbe,
665
+ hostWorkspacePolicy
666
+ );
667
+ assert(
668
+ exactReadOnlyGitIdentityProbePolicy.allowed &&
669
+ exactReadOnlyGitIdentityProbePolicy.category === "read-only" &&
670
+ exactReadOnlyGitIdentityProbePolicy.needsNetwork === false &&
671
+ exactReadOnlyGitIdentityProbePolicy.writesWorkspace === false,
672
+ "bounded read-only git identity substitutions incorrectly required destructive host permission"
673
+ );
674
+ const exactReadOnlyGitIdentityLoop =
675
+ 'for d in /home/lachlan/ProjectsLFS/AgenticApp /home/lachlan/ProjectsLFS/Musia /home/lachlan/ProjectsLFS/LALACHAN /home/lachlan/DiskMech/Projects/lazyedit; do echo "=== $d ==="; echo "user.name=[$(git -C "$d" config user.name 2>/dev/null)]"; echo "user.email=[$(git -C "$d" config user.email 2>/dev/null)]"; echo "--status--"; git -C "$d" status --short 2>&1 | head -15; echo; done';
676
+ const exactReadOnlyGitIdentityLoopPolicy = evaluateCommandPolicy(
677
+ exactReadOnlyGitIdentityLoop,
678
+ hostReadRootPolicy
679
+ );
680
+ assert(
681
+ exactReadOnlyGitIdentityLoopPolicy.allowed &&
682
+ exactReadOnlyGitIdentityLoopPolicy.category === "read-only" &&
683
+ exactReadOnlyGitIdentityLoopPolicy.boundedForLoop === true &&
684
+ exactReadOnlyGitIdentityLoopPolicy.needsNetwork === false &&
685
+ exactReadOnlyGitIdentityLoopPolicy.writesWorkspace === false,
686
+ "bounded per-repository Git identity/status audit incorrectly required destructive host permission"
687
+ );
688
+ for (const unsafeEchoSubstitution of [
689
+ 'echo "VALUE=$(rm -rf report.md)"',
690
+ 'echo "VALUE=$(cp README.md report.md)"',
691
+ 'echo "VALUE=$(curl https://example.com)"',
692
+ 'echo "VALUE=$(git status; rm -rf report.md)"',
693
+ 'echo "VALUE=$(echo $(pwd))"',
694
+ 'echo "VALUE=$((1 + 1))"',
695
+ 'echo "VALUE=$(git status)" > report.md',
696
+ 'echo "VALUE=`git status`"',
697
+ ]) {
698
+ const unsafeEchoSubstitutionPolicy = evaluateCommandPolicy(
699
+ unsafeEchoSubstitution,
700
+ hostWorkspacePolicy
701
+ );
702
+ assert(
703
+ !unsafeEchoSubstitutionPolicy.allowed ||
704
+ unsafeEchoSubstitutionPolicy.category !== "read-only" ||
705
+ unsafeEchoSubstitutionPolicy.writesWorkspace === true ||
706
+ unsafeEchoSubstitutionPolicy.needsNetwork === true,
707
+ `unsafe echo command substitution bypassed the bounded read-only policy: ${unsafeEchoSubstitution}`
708
+ );
709
+ }
710
+ for (const unsafeLoopSubstitution of [
711
+ 'for d in /tmp; do echo "VALUE=$(rm -rf report.md)"; done',
712
+ 'for d in /tmp; do echo "VALUE=$(curl https://example.com)"; done',
713
+ 'for d in /tmp; do echo "VALUE=$(echo $(pwd))"; done',
714
+ 'for d in /tmp; do echo "VALUE=$SECRET"; done',
715
+ 'for d in /tmp; do echo "VALUE=$(git -C "$d" config user.name attacker)"; done',
716
+ 'for d in /tmp; do echo "VALUE=$(git -C "$d" config --global user.name)"; done',
717
+ 'for d in /tmp; do echo "VALUE=$(git -C "$d" status; rm -rf report.md)"; done',
718
+ 'for d in /tmp; do git -C "$d" status --short > report.md; done',
719
+ ]) {
720
+ const unsafeLoopSubstitutionPolicy = evaluateCommandPolicy(
721
+ unsafeLoopSubstitution,
722
+ hostReadRootPolicy
723
+ );
724
+ assert(
725
+ !unsafeLoopSubstitutionPolicy.allowed ||
726
+ unsafeLoopSubstitutionPolicy.category !== "read-only" ||
727
+ unsafeLoopSubstitutionPolicy.writesWorkspace === true ||
728
+ unsafeLoopSubstitutionPolicy.needsNetwork === true,
729
+ `unsafe loop substitution bypassed the bounded read-only policy: ${unsafeLoopSubstitution}`
730
+ );
731
+ }
661
732
  for (const unsafeCompoundAudit of [
662
733
  'cp README.md copied.md; for d in /tmp; do echo "$d"; done',
663
734
  'echo start; for d in /tmp; do if [ -d "$d" ]; then rm -rf "$d"; else echo missing; fi; done',
@@ -30,7 +30,7 @@ const READ_ONLY_PATTERNS = [
30
30
  /^sha256sum(?:\s+[-\w./~*]+)+$/,
31
31
  /^sed\s+-n\s+['"0-9,:p\s-]+(?:\s+[-\w./~*]+)?$/,
32
32
  /^git\s+(status|branch|log|show|diff(?:\s+--stat)?|remote\s+-v)(?:\s+.+)?$/,
33
- /^git\s+rev-parse(?:\s+(?:--show-toplevel|--git-dir|--is-inside-work-tree|--show-prefix|--show-cdup|--verify|--short(?:=\d+)?|[-\w./@^{}~:]+))+$/,
33
+ /^git\s+rev-parse(?:\s+(?:--show-toplevel|--git-dir|--is-inside-work-tree|--show-prefix|--show-cdup|--abbrev-ref|--verify|--short(?:=\d+)?|[-\w./@^{}~:]+))+$/,
34
34
  /^git\s+ls-files(?:\s+(?:--(?:cached|deleted|modified|others|ignored|stage|unmerged|exclude-standard)|[-\w./*]+))*$/,
35
35
  /^node\s+(?:-v|--version)$/,
36
36
  /^node\s+[-\w./]+\.(?:c?js|mjs)\s+(?:--help|help|doctor|status|health)(?:\s+[-\w./:=]+)*$/,
@@ -1010,6 +1010,166 @@ function classifySafeEchoRedirect(normalized = "") {
1010
1010
  };
1011
1011
  }
1012
1012
 
1013
+ function extractBoundedCommandSubstitutions(value = "") {
1014
+ const text = String(value || "");
1015
+ if (!text.includes("$(") || text.length > 16 * 1024 || text.includes("`")) return null;
1016
+ const commands = [];
1017
+ let template = "";
1018
+ let quote = "";
1019
+ let escaped = false;
1020
+
1021
+ for (let index = 0; index < text.length; index += 1) {
1022
+ const char = text[index];
1023
+ if (quote === "'") {
1024
+ template += char;
1025
+ if (char === "'") quote = "";
1026
+ continue;
1027
+ }
1028
+ if (escaped) {
1029
+ template += char;
1030
+ escaped = false;
1031
+ continue;
1032
+ }
1033
+ if (char === "\\") {
1034
+ template += char;
1035
+ escaped = true;
1036
+ continue;
1037
+ }
1038
+ if (quote === '"' && char === '"') {
1039
+ template += char;
1040
+ quote = "";
1041
+ continue;
1042
+ }
1043
+ if (!quote && char === "'") {
1044
+ template += char;
1045
+ quote = char;
1046
+ continue;
1047
+ }
1048
+ if (!quote && char === '"') {
1049
+ template += char;
1050
+ quote = '"';
1051
+ continue;
1052
+ }
1053
+ if (char !== "$" || text[index + 1] !== "(") {
1054
+ template += char;
1055
+ continue;
1056
+ }
1057
+ if (text[index + 2] === "(" || commands.length >= 4) return null;
1058
+
1059
+ let inner = "";
1060
+ let innerQuote = "";
1061
+ let innerEscaped = false;
1062
+ let closed = false;
1063
+ index += 2;
1064
+ for (; index < text.length; index += 1) {
1065
+ const innerChar = text[index];
1066
+ if (innerQuote === "'") {
1067
+ inner += innerChar;
1068
+ if (innerChar === "'") innerQuote = "";
1069
+ continue;
1070
+ }
1071
+ if (innerEscaped) {
1072
+ inner += innerChar;
1073
+ innerEscaped = false;
1074
+ continue;
1075
+ }
1076
+ if (innerChar === "\\") {
1077
+ inner += innerChar;
1078
+ innerEscaped = true;
1079
+ continue;
1080
+ }
1081
+ if (innerQuote === '"') {
1082
+ inner += innerChar;
1083
+ if (innerChar === '"') innerQuote = "";
1084
+ else if (innerChar === "$" && text[index + 1] === "(") return null;
1085
+ continue;
1086
+ }
1087
+ if (innerChar === "'" || innerChar === '"') {
1088
+ inner += innerChar;
1089
+ innerQuote = innerChar;
1090
+ continue;
1091
+ }
1092
+ if (innerChar === "$" && text[index + 1] === "(") return null;
1093
+ if (innerChar === "(" || innerChar === "{") return null;
1094
+ if (innerChar === ")") {
1095
+ closed = true;
1096
+ break;
1097
+ }
1098
+ inner += innerChar;
1099
+ }
1100
+ if (!closed || innerQuote || innerEscaped || !inner.trim()) return null;
1101
+ commands.push(inner.trim());
1102
+ template += `AGINTI_READ_VALUE_${commands.length}`;
1103
+ }
1104
+ if (!commands.length || quote || escaped || hasActiveShellExpansion(template)) return null;
1105
+ return { commands, template };
1106
+ }
1107
+
1108
+ function classifySafeEchoCommandSubstitution(normalized = "") {
1109
+ if (!/^echo\s+/.test(String(normalized || ""))) return null;
1110
+ const extracted = extractBoundedCommandSubstitutions(normalized);
1111
+ if (!extracted) return null;
1112
+ const classifications = extracted.commands.map(
1113
+ (command) => classifyShellSequence(command) || classifyPipelineSequence(command) || classifySimpleCommand(command)
1114
+ );
1115
+ const blocked = classifications.find(
1116
+ (classification) => classification.category === "blocked" || classification.category === "destructive"
1117
+ );
1118
+ if (blocked) return blocked;
1119
+ if (classifications.some(
1120
+ (classification) => classification.category !== "read-only" || classification.writesWorkspace || classification.needsNetwork
1121
+ )) {
1122
+ return null;
1123
+ }
1124
+ const templateClassification = classifySimpleCommand(extracted.template);
1125
+ if (templateClassification.category !== "read-only" || templateClassification.writesWorkspace) return null;
1126
+ return {
1127
+ category: "read-only",
1128
+ needsNetwork: false,
1129
+ writesWorkspace: false,
1130
+ gitOnly: false,
1131
+ boundedCommandSubstitution: true,
1132
+ reason: `Echo uses ${extracted.commands.length} bounded read-only command substitution${extracted.commands.length === 1 ? "" : "s"}.`,
1133
+ };
1134
+ }
1135
+
1136
+ function classifyScopedReadOnlyGitProbe(normalized = "") {
1137
+ const command = stripBenignRedirections(normalized);
1138
+ if (!/^git\s+-C\s+/.test(command) || hasActiveShellExpansion(command)) return null;
1139
+
1140
+ const tokens = tokenizeShellWords(command);
1141
+ if (tokens.length < 5 || tokens[0] !== "git" || tokens[1] !== "-C") return null;
1142
+ const repository = tokens[2] || "";
1143
+ if (
1144
+ !repository ||
1145
+ repository.startsWith("-") ||
1146
+ !/^[A-Za-z0-9_@%+=:,./~+-]+$/.test(repository) ||
1147
+ /[*?\[\]{}]/.test(repository)
1148
+ ) {
1149
+ return null;
1150
+ }
1151
+
1152
+ const operation = tokens[3] || "";
1153
+ const args = tokens.slice(4);
1154
+ const safeStatus = operation === "status" && args.every((arg) =>
1155
+ ["--short", "-s", "--porcelain", "--porcelain=v1", "--porcelain=v2", "--branch", "-b"].includes(arg)
1156
+ );
1157
+ const safeIdentityConfig = operation === "config" && (
1158
+ (args.length === 1 && ["user.name", "user.email"].includes(args[0])) ||
1159
+ (args.length === 2 && args[0] === "--get" && ["user.name", "user.email"].includes(args[1]))
1160
+ );
1161
+ if (!safeStatus && !safeIdentityConfig) return null;
1162
+
1163
+ return {
1164
+ category: "read-only",
1165
+ needsNetwork: false,
1166
+ writesWorkspace: false,
1167
+ gitOnly: true,
1168
+ scopedGitProbe: true,
1169
+ reason: `Git ${operation} reads bounded repository metadata through -C.`,
1170
+ };
1171
+ }
1172
+
1013
1173
  function classifyGitCleanDryRun(normalized) {
1014
1174
  const match = normalized.match(/^git\s+clean\b([\s\S]*)$/);
1015
1175
  if (!match) return null;
@@ -1103,6 +1263,8 @@ function classifySimpleCommand(normalized) {
1103
1263
  }
1104
1264
  const gitCleanDryRun = classifyGitCleanDryRun(normalized);
1105
1265
  if (gitCleanDryRun) return gitCleanDryRun;
1266
+ const scopedReadOnlyGitProbe = classifyScopedReadOnlyGitProbe(normalized);
1267
+ if (scopedReadOnlyGitProbe) return scopedReadOnlyGitProbe;
1106
1268
  const gitWorkflowClassification = classifyGitWorkflow(normalized);
1107
1269
  if (gitWorkflowClassification) return gitWorkflowClassification;
1108
1270
  const condaRunClassification = classifyCondaRun(normalized);
@@ -1150,6 +1312,8 @@ function classifySimpleCommand(normalized) {
1150
1312
  if (envExportClassification) return envExportClassification;
1151
1313
  const echoRedirectClassification = classifySafeEchoRedirect(normalized);
1152
1314
  if (echoRedirectClassification) return echoRedirectClassification;
1315
+ const echoCommandSubstitutionClassification = classifySafeEchoCommandSubstitution(normalized);
1316
+ if (echoCommandSubstitutionClassification) return echoCommandSubstitutionClassification;
1153
1317
 
1154
1318
  if (isUnboundedRecursiveGrep(normalized)) {
1155
1319
  return {
@@ -1518,6 +1682,27 @@ function classifyReadOnlyLoopBody(bodySource = "") {
1518
1682
  };
1519
1683
  }
1520
1684
 
1685
+ function loopBodyHasOnlyBoundedReadOnlyExpansions(bodySource = "") {
1686
+ const text = String(bodySource || "").trim();
1687
+ if (!hasActiveShellExpansion(text)) return true;
1688
+ const sequence = parseTopLevelShellSequence(text);
1689
+ if (
1690
+ !sequence.commands.length ||
1691
+ sequence.openQuote ||
1692
+ sequence.trailingEscape ||
1693
+ sequence.trailingSeparator
1694
+ ) {
1695
+ return false;
1696
+ }
1697
+ return sequence.commands.every((command) => {
1698
+ if (!hasActiveShellExpansion(command)) return true;
1699
+ const classification = classifySafeEchoCommandSubstitution(command);
1700
+ return classification?.category === "read-only" &&
1701
+ classification.writesWorkspace === false &&
1702
+ classification.needsNetwork === false;
1703
+ });
1704
+ }
1705
+
1521
1706
  function classifyReadOnlyForLoop(normalized) {
1522
1707
  const text = String(normalized || "").trim();
1523
1708
  if (!/^for\s+/.test(text)) return null;
@@ -1545,10 +1730,6 @@ function classifyReadOnlyForLoop(normalized) {
1545
1730
  );
1546
1731
  }
1547
1732
 
1548
- if (hasActiveShellCommandSubstitution(bodySource)) {
1549
- return broadForLoopClassification(text, "the body uses command substitution");
1550
- }
1551
-
1552
1733
  const escapedVariable = shellIdentifierPattern(variableName);
1553
1734
  const variableReference = new RegExp(
1554
1735
  `\\$(?:\\{${escapedVariable}\\}|${escapedVariable}(?![A-Za-z0-9_]))`,
@@ -1561,7 +1742,7 @@ function classifyReadOnlyForLoop(normalized) {
1561
1742
 
1562
1743
  for (const item of items) {
1563
1744
  const expandedBody = bodySource.replace(variableReference, item);
1564
- if (hasActiveShellExpansion(expandedBody) || hasActiveShellCommandSubstitution(expandedBody)) {
1745
+ if (!loopBodyHasOnlyBoundedReadOnlyExpansions(expandedBody)) {
1565
1746
  return broadForLoopClassification(text, "the body contains expansion beyond the loop variable");
1566
1747
  }
1567
1748
  const classification = classifyReadOnlyLoopBody(expandedBody);