@lazyingart/agintiflow 0.20.223 → 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",
|
|
@@ -673,6 +673,20 @@ try {
|
|
|
673
673
|
exactReadOnlyExistenceLoopWithSuffixPolicy.writesWorkspace === false,
|
|
674
674
|
"a finite read-only existence loop with bounded list suffix incorrectly required destructive host permission"
|
|
675
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
|
+
);
|
|
676
690
|
const exactReadOnlyGitIdentityProbe =
|
|
677
691
|
'git status --short && echo "TOPLEVEL=$(git rev-parse --show-toplevel 2>&1)" && echo "BRANCH=$(git rev-parse --abbrev-ref HEAD 2>&1)"';
|
|
678
692
|
const exactReadOnlyGitIdentityProbePolicy = evaluateCommandPolicy(
|
|
@@ -747,6 +761,11 @@ try {
|
|
|
747
761
|
'for d in /tmp; do git -C "$d" status --short > report.md; done',
|
|
748
762
|
'for d in /tmp; do git -C "$d" remote add origin https://example.com/repo.git; done',
|
|
749
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',
|
|
750
769
|
]) {
|
|
751
770
|
const unsafeLoopSubstitutionPolicy = evaluateCommandPolicy(
|
|
752
771
|
unsafeLoopSubstitution,
|
package/src/command-policy.js
CHANGED
|
@@ -1543,7 +1543,7 @@ function classifyShellSequence(normalized) {
|
|
|
1543
1543
|
|
|
1544
1544
|
const READ_ONLY_FOR_LOOP_MAX_ITEMS = 64;
|
|
1545
1545
|
const READ_ONLY_FOR_LOOP_MAX_COMMANDS = 16;
|
|
1546
|
-
const READ_ONLY_FOR_LOOP_LITERAL_PATTERN = /^[A-Za-z0-9_
|
|
1546
|
+
const READ_ONLY_FOR_LOOP_LITERAL_PATTERN = /^[A-Za-z0-9_@%+=:,./~+ -]+$/;
|
|
1547
1547
|
|
|
1548
1548
|
function shellIdentifierPattern(value = "") {
|
|
1549
1549
|
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1708,6 +1708,83 @@ function loopBodyHasOnlyBoundedReadOnlyExpansions(bodySource = "") {
|
|
|
1708
1708
|
});
|
|
1709
1709
|
}
|
|
1710
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
|
+
|
|
1711
1788
|
function classifyReadOnlyForLoop(normalized) {
|
|
1712
1789
|
const text = String(normalized || "").trim();
|
|
1713
1790
|
if (!/^for\s+/.test(text)) return null;
|
|
@@ -1747,10 +1824,11 @@ function classifyReadOnlyForLoop(normalized) {
|
|
|
1747
1824
|
|
|
1748
1825
|
for (const item of items) {
|
|
1749
1826
|
const expandedBody = bodySource.replace(variableReference, item);
|
|
1750
|
-
|
|
1827
|
+
const sanitizedBody = sanitizeReadOnlyLoopAssignments(expandedBody);
|
|
1828
|
+
if (!sanitizedBody || !loopBodyHasOnlyBoundedReadOnlyExpansions(sanitizedBody)) {
|
|
1751
1829
|
return broadForLoopClassification(text, "the body contains expansion beyond the loop variable");
|
|
1752
1830
|
}
|
|
1753
|
-
const classification = classifyReadOnlyLoopBody(
|
|
1831
|
+
const classification = classifyReadOnlyLoopBody(sanitizedBody);
|
|
1754
1832
|
if (classification.category === "blocked" || classification.category === "destructive") {
|
|
1755
1833
|
return { ...classification, gitOnly: false };
|
|
1756
1834
|
}
|
|
@@ -1771,7 +1849,7 @@ function classifyReadOnlyForLoop(normalized) {
|
|
|
1771
1849
|
|
|
1772
1850
|
function classifyReadOnlyCompoundSequence(normalized) {
|
|
1773
1851
|
const text = String(normalized || "").trim();
|
|
1774
|
-
if (!text
|
|
1852
|
+
if (!text) return null;
|
|
1775
1853
|
|
|
1776
1854
|
let forIndex = findUnquotedShellWord(text, "for");
|
|
1777
1855
|
const startsAtCommandBoundary = (index) => {
|
|
@@ -1908,7 +1986,11 @@ function classifyCdCommand(normalized) {
|
|
|
1908
1986
|
if (!isSafeRelativeDir(dir) && !virtualWorkspacePath) {
|
|
1909
1987
|
return { category: "blocked", reason: `cd target must be a safe workspace-relative directory: ${dir}` };
|
|
1910
1988
|
}
|
|
1911
|
-
const innerClassification =
|
|
1989
|
+
const innerClassification = classifyReadOnlyCompoundSequence(inner.trim()) ||
|
|
1990
|
+
classifyReadOnlyForLoop(inner.trim()) ||
|
|
1991
|
+
classifyShellSequence(inner.trim()) ||
|
|
1992
|
+
classifyPipelineSequence(inner.trim()) ||
|
|
1993
|
+
classifySimpleCommand(inner.trim());
|
|
1912
1994
|
if (innerClassification.category === "blocked") return innerClassification;
|
|
1913
1995
|
return { ...innerClassification, cdDir: dir, virtualWorkspacePath };
|
|
1914
1996
|
}
|
|
@@ -1929,7 +2011,9 @@ function classifyReadOnlyRootCd(normalized, config = {}) {
|
|
|
1929
2011
|
const readOnlyRoot = configuredReadOnlyRoot(config, dir);
|
|
1930
2012
|
if (!readOnlyRoot) return null;
|
|
1931
2013
|
|
|
1932
|
-
const innerClassification =
|
|
2014
|
+
const innerClassification = classifyReadOnlyCompoundSequence(inner.trim()) ||
|
|
2015
|
+
classifyReadOnlyForLoop(inner.trim()) ||
|
|
2016
|
+
classifyShellSequence(inner.trim()) ||
|
|
1933
2017
|
classifyPipelineSequence(inner.trim()) ||
|
|
1934
2018
|
classifySimpleCommand(inner.trim());
|
|
1935
2019
|
const safeCategory = ["read-only", "network-fetch"].includes(innerClassification.category);
|