@lazyingart/agintiflow 0.20.222 → 0.20.224
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.
|
@@ -73,6 +73,7 @@ Current contract:
|
|
|
73
73
|
- `danger` is trusted host/full-access mode: host shell, destructive actions, host installs, password typing, and outside-workspace file paths are enabled. Use it only for tasks you trust; obvious secret/publish exfiltration guards remain hard stops.
|
|
74
74
|
- Workspace file tools may read and write inside the configured project folder when file tools are enabled.
|
|
75
75
|
- Narrow workspace-local shell actions such as safe probes, Gradle/TeX builds, and `chmod +x` on a project script may run when the command policy can classify them precisely.
|
|
76
|
+
- Finite literal read-only audit loops may count matches into a temporary shell variable and echo the result without asking for destructive permission. The count command, every consumer, and any surrounding command must still classify as bounded read-only work; network, mutation, dynamic item lists, and computed values used as command arguments remain blocked.
|
|
76
77
|
- Android/JVM toolchain commands may include safe local env assignments such as `ANDROID_HOME`, `ANDROID_SDK_ROOT`, `JAVA_HOME`, or `GRADLE_USER_HOME`, plus small workspace-local status-log redirects. Secret-like env vars and redirects outside the workspace remain blocked.
|
|
77
78
|
- Workspace writes outside that folder, secret paths, `.git` internals, and dependency folders such as `node_modules` are blocked.
|
|
78
79
|
- `git clone`, `git fetch`, `git pull --ff-only`, `git push`, `curl`, and `wget` are classified as network operations.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.224",
|
|
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,35 @@ 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 exactReadOnlyExistenceLoopWithSuffix =
|
|
662
|
+
'for f in scripts/xyq_cdp_browser.py scripts/xyq_chrome/launch_chrome.sh scripts/xyq_chrome/watch_thread_dom_download.py scripts/musia_mv_finalize.sh; do test -e "/home/lachlan/ProjectsLFS/LALACHAN/$f" && echo "OK $f" || echo "MISS $f"; done; echo "---mv_packs---"; ls -1 /home/lachlan/ProjectsLFS/Musia/data/mv_packs 2>&1; echo "---creative_projects---"; ls -1 /home/lachlan/ProjectsLFS/Musia/data/creative_projects 2>&1 | head -20';
|
|
663
|
+
const exactReadOnlyExistenceLoopWithSuffixPolicy = evaluateCommandPolicy(
|
|
664
|
+
exactReadOnlyExistenceLoopWithSuffix,
|
|
665
|
+
hostReadRootPolicy
|
|
666
|
+
);
|
|
667
|
+
assert(
|
|
668
|
+
exactReadOnlyExistenceLoopWithSuffixPolicy.allowed &&
|
|
669
|
+
exactReadOnlyExistenceLoopWithSuffixPolicy.category === "read-only" &&
|
|
670
|
+
exactReadOnlyExistenceLoopWithSuffixPolicy.boundedForLoop === true &&
|
|
671
|
+
exactReadOnlyExistenceLoopWithSuffixPolicy.boundedCompoundSequence === true &&
|
|
672
|
+
exactReadOnlyExistenceLoopWithSuffixPolicy.needsNetwork === false &&
|
|
673
|
+
exactReadOnlyExistenceLoopWithSuffixPolicy.writesWorkspace === false,
|
|
674
|
+
"a finite read-only existence loop with bounded list suffix incorrectly required destructive host permission"
|
|
675
|
+
);
|
|
676
|
+
const exactReadOnlyReportLiteralAudit =
|
|
677
|
+
'cd . && for s in \'handoff/LABCANVAS_AGENT_API_HANDOFF_2026_07_29.md\' \'/home/lachlan/ProjectsLFS/AgenticApp\' \'src/agenticapp/musia_ops.py\' \'scripts/xyq_chrome/watch_thread_dom_download.py\' \'Reviewed master\' \'ffprobe verification\' \'Song-only versus MV separation\' \'MV request versus publication separation\'; do n=$(grep -c -- "$s" media-routine-readiness.md); echo "$n :: $s"; done; echo \'--- git status ---\'; git status --short';
|
|
678
|
+
const exactReadOnlyReportLiteralAuditPolicy = evaluateCommandPolicy(
|
|
679
|
+
exactReadOnlyReportLiteralAudit,
|
|
680
|
+
hostWorkspacePolicy
|
|
681
|
+
);
|
|
682
|
+
assert(
|
|
683
|
+
exactReadOnlyReportLiteralAuditPolicy.allowed &&
|
|
684
|
+
exactReadOnlyReportLiteralAuditPolicy.category === "read-only" &&
|
|
685
|
+
exactReadOnlyReportLiteralAuditPolicy.boundedForLoop === true &&
|
|
686
|
+
exactReadOnlyReportLiteralAuditPolicy.needsNetwork === false &&
|
|
687
|
+
exactReadOnlyReportLiteralAuditPolicy.writesWorkspace === false,
|
|
688
|
+
"a bounded report literal-count audit incorrectly required destructive host permission"
|
|
689
|
+
);
|
|
661
690
|
const exactReadOnlyGitIdentityProbe =
|
|
662
691
|
'git status --short && echo "TOPLEVEL=$(git rev-parse --show-toplevel 2>&1)" && echo "BRANCH=$(git rev-parse --abbrev-ref HEAD 2>&1)"';
|
|
663
692
|
const exactReadOnlyGitIdentityProbePolicy = evaluateCommandPolicy(
|
|
@@ -732,6 +761,11 @@ try {
|
|
|
732
761
|
'for d in /tmp; do git -C "$d" status --short > report.md; done',
|
|
733
762
|
'for d in /tmp; do git -C "$d" remote add origin https://example.com/repo.git; done',
|
|
734
763
|
'for d in /tmp; do git -C "$d" remote set-url origin https://example.com/repo.git; done',
|
|
764
|
+
'for s in README.md; do n=$(rm -rf report.md); echo "$n :: $s"; done',
|
|
765
|
+
'for s in README.md; do n=$(curl https://example.com); echo "$n :: $s"; done',
|
|
766
|
+
'for s in README.md; do n=$(cat "$s"); echo "$n :: $s"; done',
|
|
767
|
+
'for s in README.md; do n=$(grep -c -- "$s" README.md); rm -rf "$n"; done',
|
|
768
|
+
'for s in \'README.md; rm -rf report.md\'; do echo "$s"; done',
|
|
735
769
|
]) {
|
|
736
770
|
const unsafeLoopSubstitutionPolicy = evaluateCommandPolicy(
|
|
737
771
|
unsafeLoopSubstitution,
|
|
@@ -754,6 +788,9 @@ try {
|
|
|
754
788
|
'echo start; for d in /tmp; do if [ -d "$d" ]; then if [ -f "$d/x" ]; then cat "$d/x"; else echo missing; fi; else echo absent; fi; done',
|
|
755
789
|
'echo start; for d in $ROOTS; do if [ -d "$d" ]; then ls "$d"; else echo missing; fi; done',
|
|
756
790
|
'echo start; for d in /tmp; do if [ -d "$d" ]; then ls "$d"; else echo missing; fi; done; rm -rf report.md',
|
|
791
|
+
'for f in README.md; do test -e "$f" && echo ok || echo missing; done; cp README.md copied.md',
|
|
792
|
+
'for f in README.md; do test -e "$f" && echo ok || echo missing; done; curl https://example.com',
|
|
793
|
+
'for f in README.md; do test -e "$f" && echo ok || echo missing; done; echo done > report.md',
|
|
757
794
|
]) {
|
|
758
795
|
const unsafeCompoundAuditPolicy = evaluateCommandPolicy(unsafeCompoundAudit, hostReadRootPolicy);
|
|
759
796
|
assert(
|
package/src/command-policy.js
CHANGED
|
@@ -1357,7 +1357,11 @@ function classifySimpleCommand(normalized) {
|
|
|
1357
1357
|
};
|
|
1358
1358
|
}
|
|
1359
1359
|
|
|
1360
|
-
if (
|
|
1360
|
+
if (
|
|
1361
|
+
matchAny(READ_ONLY_PATTERNS, commandForPatternMatching) ||
|
|
1362
|
+
isReadOnlyFindCommand(normalized) ||
|
|
1363
|
+
(!hasActiveShellExpansion(benignRedirectCommand) && isReadOnlyShellCondition(benignRedirectCommand))
|
|
1364
|
+
) {
|
|
1361
1365
|
return {
|
|
1362
1366
|
category: "read-only",
|
|
1363
1367
|
needsNetwork: false,
|
|
@@ -1539,7 +1543,7 @@ function classifyShellSequence(normalized) {
|
|
|
1539
1543
|
|
|
1540
1544
|
const READ_ONLY_FOR_LOOP_MAX_ITEMS = 64;
|
|
1541
1545
|
const READ_ONLY_FOR_LOOP_MAX_COMMANDS = 16;
|
|
1542
|
-
const READ_ONLY_FOR_LOOP_LITERAL_PATTERN = /^[A-Za-z0-9_
|
|
1546
|
+
const READ_ONLY_FOR_LOOP_LITERAL_PATTERN = /^[A-Za-z0-9_@%+=:,./~+ -]+$/;
|
|
1543
1547
|
|
|
1544
1548
|
function shellIdentifierPattern(value = "") {
|
|
1545
1549
|
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1704,6 +1708,83 @@ function loopBodyHasOnlyBoundedReadOnlyExpansions(bodySource = "") {
|
|
|
1704
1708
|
});
|
|
1705
1709
|
}
|
|
1706
1710
|
|
|
1711
|
+
function isBoundedReadOnlyScalarCommand(command = "") {
|
|
1712
|
+
const normalized = stripBenignRedirections(command);
|
|
1713
|
+
if (!normalized || hasActiveShellExpansion(normalized)) return false;
|
|
1714
|
+
const tokens = tokenizeShellWords(normalized);
|
|
1715
|
+
if (!tokens.length) return false;
|
|
1716
|
+
if (["grep", "rg"].includes(tokens[0])) {
|
|
1717
|
+
return tokens.slice(1).some((token) =>
|
|
1718
|
+
token === "--count" || /^-[A-Za-z]*c[A-Za-z]*$/.test(token)
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
return tokens[0] === "wc" && tokens.slice(1).includes("-l");
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function sanitizeReadOnlyLoopAssignments(bodySource = "") {
|
|
1725
|
+
const text = String(bodySource || "").trim();
|
|
1726
|
+
const sequence = parseTopLevelShellSequence(text);
|
|
1727
|
+
if (
|
|
1728
|
+
!sequence.commands.length ||
|
|
1729
|
+
sequence.openQuote ||
|
|
1730
|
+
sequence.trailingEscape ||
|
|
1731
|
+
sequence.trailingSeparator
|
|
1732
|
+
) {
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
const assigned = new Map();
|
|
1737
|
+
const commands = [];
|
|
1738
|
+
for (const sourceCommand of sequence.commands) {
|
|
1739
|
+
let command = sourceCommand;
|
|
1740
|
+
for (const [name, state] of assigned.entries()) {
|
|
1741
|
+
const escapedName = shellIdentifierPattern(name);
|
|
1742
|
+
const reference = new RegExp(`\\$(?:\\{${escapedName}\\}|${escapedName}(?![A-Za-z0-9_]))`, "g");
|
|
1743
|
+
if (!reference.test(command)) continue;
|
|
1744
|
+
if (!/^echo\s+/.test(command)) return null;
|
|
1745
|
+
command = command.replace(reference, `AGINTI_READ_VALUE_${name}`);
|
|
1746
|
+
state.used = true;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
const assignment = command.match(/^([A-Za-z_][A-Za-z0-9_]*)=(\$\([\s\S]+\))$/);
|
|
1750
|
+
if (!assignment) {
|
|
1751
|
+
commands.push(command);
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
const [, name, expression] = assignment;
|
|
1755
|
+
if (assigned.has(name) || assigned.size >= 4) return null;
|
|
1756
|
+
const extracted = extractBoundedCommandSubstitutions(`echo ${expression}`);
|
|
1757
|
+
if (
|
|
1758
|
+
!extracted ||
|
|
1759
|
+
extracted.commands.length !== 1 ||
|
|
1760
|
+
extracted.template !== "echo AGINTI_READ_VALUE_1"
|
|
1761
|
+
) {
|
|
1762
|
+
return null;
|
|
1763
|
+
}
|
|
1764
|
+
const inner = extracted.commands[0];
|
|
1765
|
+
const classification = classifyShellSequence(inner) ||
|
|
1766
|
+
classifyPipelineSequence(inner) ||
|
|
1767
|
+
classifySimpleCommand(inner);
|
|
1768
|
+
if (
|
|
1769
|
+
classification.category !== "read-only" ||
|
|
1770
|
+
classification.writesWorkspace ||
|
|
1771
|
+
classification.needsNetwork ||
|
|
1772
|
+
!isBoundedReadOnlyScalarCommand(inner)
|
|
1773
|
+
) {
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
assigned.set(name, { used: false });
|
|
1777
|
+
commands.push("true");
|
|
1778
|
+
}
|
|
1779
|
+
if ([...assigned.values()].some((state) => !state.used)) return null;
|
|
1780
|
+
|
|
1781
|
+
let sanitized = commands[0];
|
|
1782
|
+
for (let index = 1; index < commands.length; index += 1) {
|
|
1783
|
+
sanitized += ` ${sequence.separators[index - 1] || ";"} ${commands[index]}`;
|
|
1784
|
+
}
|
|
1785
|
+
return sanitized;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1707
1788
|
function classifyReadOnlyForLoop(normalized) {
|
|
1708
1789
|
const text = String(normalized || "").trim();
|
|
1709
1790
|
if (!/^for\s+/.test(text)) return null;
|
|
@@ -1743,10 +1824,11 @@ function classifyReadOnlyForLoop(normalized) {
|
|
|
1743
1824
|
|
|
1744
1825
|
for (const item of items) {
|
|
1745
1826
|
const expandedBody = bodySource.replace(variableReference, item);
|
|
1746
|
-
|
|
1827
|
+
const sanitizedBody = sanitizeReadOnlyLoopAssignments(expandedBody);
|
|
1828
|
+
if (!sanitizedBody || !loopBodyHasOnlyBoundedReadOnlyExpansions(sanitizedBody)) {
|
|
1747
1829
|
return broadForLoopClassification(text, "the body contains expansion beyond the loop variable");
|
|
1748
1830
|
}
|
|
1749
|
-
const classification = classifyReadOnlyLoopBody(
|
|
1831
|
+
const classification = classifyReadOnlyLoopBody(sanitizedBody);
|
|
1750
1832
|
if (classification.category === "blocked" || classification.category === "destructive") {
|
|
1751
1833
|
return { ...classification, gitOnly: false };
|
|
1752
1834
|
}
|
|
@@ -1767,7 +1849,7 @@ function classifyReadOnlyForLoop(normalized) {
|
|
|
1767
1849
|
|
|
1768
1850
|
function classifyReadOnlyCompoundSequence(normalized) {
|
|
1769
1851
|
const text = String(normalized || "").trim();
|
|
1770
|
-
if (!text
|
|
1852
|
+
if (!text) return null;
|
|
1771
1853
|
|
|
1772
1854
|
let forIndex = findUnquotedShellWord(text, "for");
|
|
1773
1855
|
const startsAtCommandBoundary = (index) => {
|
|
@@ -1778,16 +1860,24 @@ function classifyReadOnlyCompoundSequence(normalized) {
|
|
|
1778
1860
|
while (forIndex > 0 && !startsAtCommandBoundary(forIndex)) {
|
|
1779
1861
|
forIndex = findUnquotedShellWord(text, "for", forIndex + 3);
|
|
1780
1862
|
}
|
|
1781
|
-
if (forIndex
|
|
1863
|
+
if (forIndex < 0) return null;
|
|
1782
1864
|
|
|
1783
1865
|
const prefixSource = trimShellListBoundary(text.slice(0, forIndex));
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1866
|
+
const doneIndex = findUnquotedShellWord(text, "done", forIndex + 3);
|
|
1867
|
+
if (doneIndex < 0) return null;
|
|
1868
|
+
const loopSource = text.slice(forIndex, doneIndex + 4).trim();
|
|
1869
|
+
const suffixSource = trimShellListBoundary(text.slice(doneIndex + 4));
|
|
1870
|
+
if (!prefixSource && !suffixSource) return null;
|
|
1871
|
+
|
|
1872
|
+
for (const [label, source] of [["prelude", prefixSource], ["suffix", suffixSource]]) {
|
|
1873
|
+
if (!source) continue;
|
|
1874
|
+
const classification = classifyReadOnlyCommandList(source);
|
|
1875
|
+
if (classification.category === "blocked" || classification.category === "destructive") {
|
|
1876
|
+
return { ...classification, gitOnly: false };
|
|
1877
|
+
}
|
|
1878
|
+
if (classification.category !== "read-only" || classification.writesWorkspace) {
|
|
1879
|
+
return broadForLoopClassification(text, `the command ${label} is not read-only`);
|
|
1880
|
+
}
|
|
1791
1881
|
}
|
|
1792
1882
|
const loopClassification = classifyReadOnlyForLoop(loopSource);
|
|
1793
1883
|
if (!loopClassification) return null;
|
|
@@ -1801,7 +1891,7 @@ function classifyReadOnlyCompoundSequence(normalized) {
|
|
|
1801
1891
|
gitOnly: false,
|
|
1802
1892
|
boundedForLoop: true,
|
|
1803
1893
|
boundedCompoundSequence: true,
|
|
1804
|
-
reason: "
|
|
1894
|
+
reason: "A bounded read-only shell loop has only finite read-only command context.",
|
|
1805
1895
|
};
|
|
1806
1896
|
}
|
|
1807
1897
|
|
|
@@ -1896,7 +1986,11 @@ function classifyCdCommand(normalized) {
|
|
|
1896
1986
|
if (!isSafeRelativeDir(dir) && !virtualWorkspacePath) {
|
|
1897
1987
|
return { category: "blocked", reason: `cd target must be a safe workspace-relative directory: ${dir}` };
|
|
1898
1988
|
}
|
|
1899
|
-
const innerClassification =
|
|
1989
|
+
const innerClassification = classifyReadOnlyCompoundSequence(inner.trim()) ||
|
|
1990
|
+
classifyReadOnlyForLoop(inner.trim()) ||
|
|
1991
|
+
classifyShellSequence(inner.trim()) ||
|
|
1992
|
+
classifyPipelineSequence(inner.trim()) ||
|
|
1993
|
+
classifySimpleCommand(inner.trim());
|
|
1900
1994
|
if (innerClassification.category === "blocked") return innerClassification;
|
|
1901
1995
|
return { ...innerClassification, cdDir: dir, virtualWorkspacePath };
|
|
1902
1996
|
}
|
|
@@ -1917,7 +2011,9 @@ function classifyReadOnlyRootCd(normalized, config = {}) {
|
|
|
1917
2011
|
const readOnlyRoot = configuredReadOnlyRoot(config, dir);
|
|
1918
2012
|
if (!readOnlyRoot) return null;
|
|
1919
2013
|
|
|
1920
|
-
const innerClassification =
|
|
2014
|
+
const innerClassification = classifyReadOnlyCompoundSequence(inner.trim()) ||
|
|
2015
|
+
classifyReadOnlyForLoop(inner.trim()) ||
|
|
2016
|
+
classifyShellSequence(inner.trim()) ||
|
|
1921
2017
|
classifyPipelineSequence(inner.trim()) ||
|
|
1922
2018
|
classifySimpleCommand(inner.trim());
|
|
1923
2019
|
const safeCategory = ["read-only", "network-fetch"].includes(innerClassification.category);
|