@lazyingart/agintiflow 0.20.220 → 0.20.222
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 +52 -0
- package/src/command-policy.js +62 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.222",
|
|
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,34 @@ 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
|
+
);
|
|
688
|
+
const exactReadOnlyGitRepositoryAudit =
|
|
689
|
+
'for d in /home/lachlan/ProjectsLFS/AgenticApp /home/lachlan/ProjectsLFS/Musia /home/lachlan/ProjectsLFS/LALACHAN /home/lachlan/DiskMech/Projects/lazyedit; do echo "=== $d ==="; git -C "$d" config --get user.name; git -C "$d" config --get user.email; git -C "$d" status --porcelain=v1 --branch; echo "--- remotes ---"; git -C "$d" remote -v; echo; done';
|
|
690
|
+
const exactReadOnlyGitRepositoryAuditPolicy = evaluateCommandPolicy(
|
|
691
|
+
exactReadOnlyGitRepositoryAudit,
|
|
692
|
+
hostReadRootPolicy
|
|
693
|
+
);
|
|
694
|
+
assert(
|
|
695
|
+
exactReadOnlyGitRepositoryAuditPolicy.allowed &&
|
|
696
|
+
exactReadOnlyGitRepositoryAuditPolicy.category === "read-only" &&
|
|
697
|
+
exactReadOnlyGitRepositoryAuditPolicy.boundedForLoop === true &&
|
|
698
|
+
exactReadOnlyGitRepositoryAuditPolicy.needsNetwork === false &&
|
|
699
|
+
exactReadOnlyGitRepositoryAuditPolicy.writesWorkspace === false,
|
|
700
|
+
"bounded per-repository Git identity/status/remote audit incorrectly required destructive host permission"
|
|
701
|
+
);
|
|
674
702
|
for (const unsafeEchoSubstitution of [
|
|
675
703
|
'echo "VALUE=$(rm -rf report.md)"',
|
|
676
704
|
'echo "VALUE=$(cp README.md report.md)"',
|
|
@@ -693,6 +721,30 @@ try {
|
|
|
693
721
|
`unsafe echo command substitution bypassed the bounded read-only policy: ${unsafeEchoSubstitution}`
|
|
694
722
|
);
|
|
695
723
|
}
|
|
724
|
+
for (const unsafeLoopSubstitution of [
|
|
725
|
+
'for d in /tmp; do echo "VALUE=$(rm -rf report.md)"; done',
|
|
726
|
+
'for d in /tmp; do echo "VALUE=$(curl https://example.com)"; done',
|
|
727
|
+
'for d in /tmp; do echo "VALUE=$(echo $(pwd))"; done',
|
|
728
|
+
'for d in /tmp; do echo "VALUE=$SECRET"; done',
|
|
729
|
+
'for d in /tmp; do echo "VALUE=$(git -C "$d" config user.name attacker)"; done',
|
|
730
|
+
'for d in /tmp; do echo "VALUE=$(git -C "$d" config --global user.name)"; done',
|
|
731
|
+
'for d in /tmp; do echo "VALUE=$(git -C "$d" status; rm -rf report.md)"; done',
|
|
732
|
+
'for d in /tmp; do git -C "$d" status --short > report.md; done',
|
|
733
|
+
'for d in /tmp; do git -C "$d" remote add origin https://example.com/repo.git; done',
|
|
734
|
+
'for d in /tmp; do git -C "$d" remote set-url origin https://example.com/repo.git; done',
|
|
735
|
+
]) {
|
|
736
|
+
const unsafeLoopSubstitutionPolicy = evaluateCommandPolicy(
|
|
737
|
+
unsafeLoopSubstitution,
|
|
738
|
+
hostReadRootPolicy
|
|
739
|
+
);
|
|
740
|
+
assert(
|
|
741
|
+
!unsafeLoopSubstitutionPolicy.allowed ||
|
|
742
|
+
unsafeLoopSubstitutionPolicy.category !== "read-only" ||
|
|
743
|
+
unsafeLoopSubstitutionPolicy.writesWorkspace === true ||
|
|
744
|
+
unsafeLoopSubstitutionPolicy.needsNetwork === true,
|
|
745
|
+
`unsafe loop substitution bypassed the bounded read-only policy: ${unsafeLoopSubstitution}`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
696
748
|
for (const unsafeCompoundAudit of [
|
|
697
749
|
'cp README.md copied.md; for d in /tmp; do echo "$d"; done',
|
|
698
750
|
'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,44 @@ 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
|
+
const safeRemoteList = operation === "remote" && args.length === 1 && args[0] === "-v";
|
|
1162
|
+
if (!safeStatus && !safeIdentityConfig && !safeRemoteList) return null;
|
|
1163
|
+
|
|
1164
|
+
return {
|
|
1165
|
+
category: "read-only",
|
|
1166
|
+
needsNetwork: false,
|
|
1167
|
+
writesWorkspace: false,
|
|
1168
|
+
gitOnly: true,
|
|
1169
|
+
scopedGitProbe: true,
|
|
1170
|
+
reason: `Git ${operation} reads bounded repository metadata through -C.`,
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1136
1174
|
function classifyGitCleanDryRun(normalized) {
|
|
1137
1175
|
const match = normalized.match(/^git\s+clean\b([\s\S]*)$/);
|
|
1138
1176
|
if (!match) return null;
|
|
@@ -1226,6 +1264,8 @@ function classifySimpleCommand(normalized) {
|
|
|
1226
1264
|
}
|
|
1227
1265
|
const gitCleanDryRun = classifyGitCleanDryRun(normalized);
|
|
1228
1266
|
if (gitCleanDryRun) return gitCleanDryRun;
|
|
1267
|
+
const scopedReadOnlyGitProbe = classifyScopedReadOnlyGitProbe(normalized);
|
|
1268
|
+
if (scopedReadOnlyGitProbe) return scopedReadOnlyGitProbe;
|
|
1229
1269
|
const gitWorkflowClassification = classifyGitWorkflow(normalized);
|
|
1230
1270
|
if (gitWorkflowClassification) return gitWorkflowClassification;
|
|
1231
1271
|
const condaRunClassification = classifyCondaRun(normalized);
|
|
@@ -1643,6 +1683,27 @@ function classifyReadOnlyLoopBody(bodySource = "") {
|
|
|
1643
1683
|
};
|
|
1644
1684
|
}
|
|
1645
1685
|
|
|
1686
|
+
function loopBodyHasOnlyBoundedReadOnlyExpansions(bodySource = "") {
|
|
1687
|
+
const text = String(bodySource || "").trim();
|
|
1688
|
+
if (!hasActiveShellExpansion(text)) return true;
|
|
1689
|
+
const sequence = parseTopLevelShellSequence(text);
|
|
1690
|
+
if (
|
|
1691
|
+
!sequence.commands.length ||
|
|
1692
|
+
sequence.openQuote ||
|
|
1693
|
+
sequence.trailingEscape ||
|
|
1694
|
+
sequence.trailingSeparator
|
|
1695
|
+
) {
|
|
1696
|
+
return false;
|
|
1697
|
+
}
|
|
1698
|
+
return sequence.commands.every((command) => {
|
|
1699
|
+
if (!hasActiveShellExpansion(command)) return true;
|
|
1700
|
+
const classification = classifySafeEchoCommandSubstitution(command);
|
|
1701
|
+
return classification?.category === "read-only" &&
|
|
1702
|
+
classification.writesWorkspace === false &&
|
|
1703
|
+
classification.needsNetwork === false;
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1646
1707
|
function classifyReadOnlyForLoop(normalized) {
|
|
1647
1708
|
const text = String(normalized || "").trim();
|
|
1648
1709
|
if (!/^for\s+/.test(text)) return null;
|
|
@@ -1670,10 +1731,6 @@ function classifyReadOnlyForLoop(normalized) {
|
|
|
1670
1731
|
);
|
|
1671
1732
|
}
|
|
1672
1733
|
|
|
1673
|
-
if (hasActiveShellCommandSubstitution(bodySource)) {
|
|
1674
|
-
return broadForLoopClassification(text, "the body uses command substitution");
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
1734
|
const escapedVariable = shellIdentifierPattern(variableName);
|
|
1678
1735
|
const variableReference = new RegExp(
|
|
1679
1736
|
`\\$(?:\\{${escapedVariable}\\}|${escapedVariable}(?![A-Za-z0-9_]))`,
|
|
@@ -1686,7 +1743,7 @@ function classifyReadOnlyForLoop(normalized) {
|
|
|
1686
1743
|
|
|
1687
1744
|
for (const item of items) {
|
|
1688
1745
|
const expandedBody = bodySource.replace(variableReference, item);
|
|
1689
|
-
if (
|
|
1746
|
+
if (!loopBodyHasOnlyBoundedReadOnlyExpansions(expandedBody)) {
|
|
1690
1747
|
return broadForLoopClassification(text, "the body contains expansion beyond the loop variable");
|
|
1691
1748
|
}
|
|
1692
1749
|
const classification = classifyReadOnlyLoopBody(expandedBody);
|