@lazyingart/agintiflow 0.20.219 → 0.20.220

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.220",
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,41 @@ 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
+ for (const unsafeEchoSubstitution of [
675
+ 'echo "VALUE=$(rm -rf report.md)"',
676
+ 'echo "VALUE=$(cp README.md report.md)"',
677
+ 'echo "VALUE=$(curl https://example.com)"',
678
+ 'echo "VALUE=$(git status; rm -rf report.md)"',
679
+ 'echo "VALUE=$(echo $(pwd))"',
680
+ 'echo "VALUE=$((1 + 1))"',
681
+ 'echo "VALUE=$(git status)" > report.md',
682
+ 'echo "VALUE=`git status`"',
683
+ ]) {
684
+ const unsafeEchoSubstitutionPolicy = evaluateCommandPolicy(
685
+ unsafeEchoSubstitution,
686
+ hostWorkspacePolicy
687
+ );
688
+ assert(
689
+ !unsafeEchoSubstitutionPolicy.allowed ||
690
+ unsafeEchoSubstitutionPolicy.category !== "read-only" ||
691
+ unsafeEchoSubstitutionPolicy.writesWorkspace === true ||
692
+ unsafeEchoSubstitutionPolicy.needsNetwork === true,
693
+ `unsafe echo command substitution bypassed the bounded read-only policy: ${unsafeEchoSubstitution}`
694
+ );
695
+ }
661
696
  for (const unsafeCompoundAudit of [
662
697
  'cp README.md copied.md; for d in /tmp; do echo "$d"; done',
663
698
  '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,129 @@ 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
+
1013
1136
  function classifyGitCleanDryRun(normalized) {
1014
1137
  const match = normalized.match(/^git\s+clean\b([\s\S]*)$/);
1015
1138
  if (!match) return null;
@@ -1150,6 +1273,8 @@ function classifySimpleCommand(normalized) {
1150
1273
  if (envExportClassification) return envExportClassification;
1151
1274
  const echoRedirectClassification = classifySafeEchoRedirect(normalized);
1152
1275
  if (echoRedirectClassification) return echoRedirectClassification;
1276
+ const echoCommandSubstitutionClassification = classifySafeEchoCommandSubstitution(normalized);
1277
+ if (echoCommandSubstitutionClassification) return echoCommandSubstitutionClassification;
1153
1278
 
1154
1279
  if (isUnboundedRecursiveGrep(normalized)) {
1155
1280
  return {