@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 +1 -1
- package/scripts/smoke-coding-tools.js +36 -0
- package/src/command-policy.js +61 -5
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",
|
|
@@ -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',
|
package/src/command-policy.js
CHANGED
|
@@ -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 (
|
|
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);
|