@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.223",
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,
@@ -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
- if (!loopBodyHasOnlyBoundedReadOnlyExpansions(expandedBody)) {
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(expandedBody);
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 || hasActiveShellCommandSubstitution(text)) return null;
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 = 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());
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 = classifyShellSequence(inner.trim()) ||
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);