@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.222",
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(
@@ -1357,7 +1357,11 @@ function classifySimpleCommand(normalized) {
1357
1357
  };
1358
1358
  }
1359
1359
 
1360
- if (matchAny(READ_ONLY_PATTERNS, commandForPatternMatching) || isReadOnlyFindCommand(normalized)) {
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
- if (!loopBodyHasOnlyBoundedReadOnlyExpansions(expandedBody)) {
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(expandedBody);
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 || /^for\s+/.test(text) || hasActiveShellCommandSubstitution(text)) return null;
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 <= 0) return null;
1863
+ if (forIndex < 0) return null;
1782
1864
 
1783
1865
  const prefixSource = trimShellListBoundary(text.slice(0, forIndex));
1784
- const loopSource = text.slice(forIndex).trim();
1785
- const prefixClassification = classifyReadOnlyCommandList(prefixSource);
1786
- if (prefixClassification.category === "blocked" || prefixClassification.category === "destructive") {
1787
- return { ...prefixClassification, gitOnly: false };
1788
- }
1789
- if (prefixClassification.category !== "read-only" || prefixClassification.writesWorkspace) {
1790
- return broadForLoopClassification(text, "the command prelude is not read-only");
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: "Finite read-only command prelude followed by a bounded read-only shell loop.",
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 = classifyShellSequence(inner.trim()) || classifyPipelineSequence(inner.trim()) || classifySimpleCommand(inner.trim());
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 = classifyShellSequence(inner.trim()) ||
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);