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