@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 +1 -1
- package/scripts/smoke-coding-tools.js +35 -0
- package/src/command-policy.js +126 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
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',
|
package/src/command-policy.js
CHANGED
|
@@ -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 {
|