@lazyingart/agintiflow 0.20.220 → 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.220",
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",
@@ -671,6 +671,20 @@ try {
671
671
  exactReadOnlyGitIdentityProbePolicy.writesWorkspace === false,
672
672
  "bounded read-only git identity substitutions incorrectly required destructive host permission"
673
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
+ );
674
688
  for (const unsafeEchoSubstitution of [
675
689
  'echo "VALUE=$(rm -rf report.md)"',
676
690
  'echo "VALUE=$(cp README.md report.md)"',
@@ -693,6 +707,28 @@ try {
693
707
  `unsafe echo command substitution bypassed the bounded read-only policy: ${unsafeEchoSubstitution}`
694
708
  );
695
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
+ }
696
732
  for (const unsafeCompoundAudit of [
697
733
  'cp README.md copied.md; for d in /tmp; do echo "$d"; done',
698
734
  'echo start; for d in /tmp; do if [ -d "$d" ]; then rm -rf "$d"; else echo missing; fi; done',
@@ -1133,6 +1133,43 @@ function classifySafeEchoCommandSubstitution(normalized = "") {
1133
1133
  };
1134
1134
  }
1135
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
+
1136
1173
  function classifyGitCleanDryRun(normalized) {
1137
1174
  const match = normalized.match(/^git\s+clean\b([\s\S]*)$/);
1138
1175
  if (!match) return null;
@@ -1226,6 +1263,8 @@ function classifySimpleCommand(normalized) {
1226
1263
  }
1227
1264
  const gitCleanDryRun = classifyGitCleanDryRun(normalized);
1228
1265
  if (gitCleanDryRun) return gitCleanDryRun;
1266
+ const scopedReadOnlyGitProbe = classifyScopedReadOnlyGitProbe(normalized);
1267
+ if (scopedReadOnlyGitProbe) return scopedReadOnlyGitProbe;
1229
1268
  const gitWorkflowClassification = classifyGitWorkflow(normalized);
1230
1269
  if (gitWorkflowClassification) return gitWorkflowClassification;
1231
1270
  const condaRunClassification = classifyCondaRun(normalized);
@@ -1643,6 +1682,27 @@ function classifyReadOnlyLoopBody(bodySource = "") {
1643
1682
  };
1644
1683
  }
1645
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
+
1646
1706
  function classifyReadOnlyForLoop(normalized) {
1647
1707
  const text = String(normalized || "").trim();
1648
1708
  if (!/^for\s+/.test(text)) return null;
@@ -1670,10 +1730,6 @@ function classifyReadOnlyForLoop(normalized) {
1670
1730
  );
1671
1731
  }
1672
1732
 
1673
- if (hasActiveShellCommandSubstitution(bodySource)) {
1674
- return broadForLoopClassification(text, "the body uses command substitution");
1675
- }
1676
-
1677
1733
  const escapedVariable = shellIdentifierPattern(variableName);
1678
1734
  const variableReference = new RegExp(
1679
1735
  `\\$(?:\\{${escapedVariable}\\}|${escapedVariable}(?![A-Za-z0-9_]))`,
@@ -1686,7 +1742,7 @@ function classifyReadOnlyForLoop(normalized) {
1686
1742
 
1687
1743
  for (const item of items) {
1688
1744
  const expandedBody = bodySource.replace(variableReference, item);
1689
- if (hasActiveShellExpansion(expandedBody) || hasActiveShellCommandSubstitution(expandedBody)) {
1745
+ if (!loopBodyHasOnlyBoundedReadOnlyExpansions(expandedBody)) {
1690
1746
  return broadForLoopClassification(text, "the body contains expansion beyond the loop variable");
1691
1747
  }
1692
1748
  const classification = classifyReadOnlyLoopBody(expandedBody);