@lazyingart/agintiflow 0.20.224 → 0.20.226
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,7 +73,8 @@ 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
|
|
76
|
+
- Finite literal read-only audits may bind a small literal path, inspect/hash it, count matches through a bounded read-only pipeline, summarize stdin with `sort | uniq -c`, and display numeric results with `echo` or `printf` without asking for destructive permission. Shell line continuations are accepted for finite literal item lists. Every producer, consumer, and surrounding command must still classify as bounded read-only work; network access, redirects, output-file operands, secret/dynamic expansion, shell-state `printf -v`, mutation, dynamic item lists, and computed values used as executable arguments remain blocked.
|
|
77
|
+
- Completion checks distinguish a resolved historical state such as "previously paused" from work that is currently pending or incomplete. A resolved past blocker therefore does not force another verification loop, while any explicit current unfinished state still does.
|
|
77
78
|
- 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.
|
|
78
79
|
- Workspace writes outside that folder, secret paths, `.git` internals, and dependency folders such as `node_modules` are blocked.
|
|
79
80
|
- `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.226",
|
|
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",
|
|
@@ -687,6 +687,46 @@ try {
|
|
|
687
687
|
exactReadOnlyReportLiteralAuditPolicy.writesWorkspace === false,
|
|
688
688
|
"a bounded report literal-count audit incorrectly required destructive host permission"
|
|
689
689
|
);
|
|
690
|
+
const exactReadOnlyFormattedReportAudit = [
|
|
691
|
+
"f=media-routine-readiness.md",
|
|
692
|
+
"printf 'lines\\tbytes\\tsha256\\n'",
|
|
693
|
+
'wc -lc "$f"',
|
|
694
|
+
'sha256sum "$f"',
|
|
695
|
+
"printf '\\n--- literal occurrence counts (bounded) ---\\n'",
|
|
696
|
+
"for s in \\",
|
|
697
|
+
" '/home/lachlan/ProjectsLFS/Musia' \\",
|
|
698
|
+
" '/home/lachlan/ProjectsLFS/AgenticApp' \\",
|
|
699
|
+
" 'music generation' \\",
|
|
700
|
+
"; do",
|
|
701
|
+
' n=$(grep -oF -- "$s" "$f" | wc -l | tr -d \' \')',
|
|
702
|
+
' printf \'%-58s %s\\n\' "$s" "$n"',
|
|
703
|
+
"done",
|
|
704
|
+
].join("\n");
|
|
705
|
+
const exactReadOnlyFormattedReportAuditPolicy = evaluateCommandPolicy(
|
|
706
|
+
exactReadOnlyFormattedReportAudit,
|
|
707
|
+
hostWorkspacePolicy
|
|
708
|
+
);
|
|
709
|
+
assert(
|
|
710
|
+
exactReadOnlyFormattedReportAuditPolicy.allowed &&
|
|
711
|
+
exactReadOnlyFormattedReportAuditPolicy.category === "read-only" &&
|
|
712
|
+
exactReadOnlyFormattedReportAuditPolicy.boundedForLoop === true &&
|
|
713
|
+
exactReadOnlyFormattedReportAuditPolicy.needsNetwork === false &&
|
|
714
|
+
exactReadOnlyFormattedReportAuditPolicy.writesWorkspace === false,
|
|
715
|
+
"a literal-file formatted count audit incorrectly required destructive host permission"
|
|
716
|
+
);
|
|
717
|
+
const exactReadOnlyMultiPatternSummary =
|
|
718
|
+
"grep -oF -e 'handoff/report.md' -e 'Reviewed master' -e 'ffprobe' report.md | sort | uniq -c";
|
|
719
|
+
const exactReadOnlyMultiPatternSummaryPolicy = evaluateCommandPolicy(
|
|
720
|
+
exactReadOnlyMultiPatternSummary,
|
|
721
|
+
hostWorkspacePolicy
|
|
722
|
+
);
|
|
723
|
+
assert(
|
|
724
|
+
exactReadOnlyMultiPatternSummaryPolicy.allowed &&
|
|
725
|
+
exactReadOnlyMultiPatternSummaryPolicy.category === "read-only" &&
|
|
726
|
+
exactReadOnlyMultiPatternSummaryPolicy.needsNetwork === false &&
|
|
727
|
+
exactReadOnlyMultiPatternSummaryPolicy.writesWorkspace === false,
|
|
728
|
+
"a bounded grep/sort/uniq count summary incorrectly required destructive host permission"
|
|
729
|
+
);
|
|
690
730
|
const exactReadOnlyGitIdentityProbe =
|
|
691
731
|
'git status --short && echo "TOPLEVEL=$(git rev-parse --show-toplevel 2>&1)" && echo "BRANCH=$(git rev-parse --abbrev-ref HEAD 2>&1)"';
|
|
692
732
|
const exactReadOnlyGitIdentityProbePolicy = evaluateCommandPolicy(
|
|
@@ -765,6 +805,14 @@ try {
|
|
|
765
805
|
'for s in README.md; do n=$(curl https://example.com); echo "$n :: $s"; done',
|
|
766
806
|
'for s in README.md; do n=$(cat "$s"); echo "$n :: $s"; done',
|
|
767
807
|
'for s in README.md; do n=$(grep -c -- "$s" README.md); rm -rf "$n"; done',
|
|
808
|
+
'f=README.md; printf "%s\\n" "$(cat .aginti/.env)"; for s in one; do echo "$s"; done',
|
|
809
|
+
'f=README.md; printf "%s\\n" "$f" > copied.md; for s in one; do echo "$s"; done',
|
|
810
|
+
'f=$(cat README.md); printf "%s\\n" "$f"; for s in one; do echo "$s"; done',
|
|
811
|
+
'f=README.md; for s in one; do n=$(grep -oF -- "$s" "$f" | wc -l | tr -d " "); printf -v target "%s" "$n"; done',
|
|
812
|
+
'f=README.md; for s in one; do n=$(grep -oF -- "$s" "$f" | wc -l | tr -d " "); rm -rf "$n"; done',
|
|
813
|
+
"grep -oF -e 'term' report.md | sort | uniq -c output.txt",
|
|
814
|
+
"grep -oF -e 'term' report.md | sort | uniq -c > output.txt",
|
|
815
|
+
'grep -oF -e "$(cat .aginti/.env)" report.md | sort | uniq -c',
|
|
768
816
|
'for s in \'README.md; rm -rf report.md\'; do echo "$s"; done',
|
|
769
817
|
]) {
|
|
770
818
|
const unsafeLoopSubstitutionPolicy = evaluateCommandPolicy(
|
|
@@ -7,11 +7,41 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { runAgent } from "../src/agent-runner.js";
|
|
8
8
|
import { resolveRuntimeConfig } from "../src/config.js";
|
|
9
9
|
import { SessionStore } from "../src/session-store.js";
|
|
10
|
+
import { finishResultClaimsIncompleteWork } from "../src/scs-evidence.js";
|
|
10
11
|
|
|
11
12
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
13
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-truthful-completion-"));
|
|
13
14
|
process.env.AGINTIFLOW_HOME = path.join(tempRoot, "home");
|
|
14
15
|
|
|
16
|
+
assert.equal(
|
|
17
|
+
finishResultClaimsIncompleteWork(
|
|
18
|
+
"The report is complete and the verification that was previously paused now passes. No work remains."
|
|
19
|
+
),
|
|
20
|
+
false,
|
|
21
|
+
"historical paused wording was mistaken for current unfinished work"
|
|
22
|
+
);
|
|
23
|
+
assert.equal(
|
|
24
|
+
finishResultClaimsIncompleteWork(
|
|
25
|
+
"The earlier step was paused, but the report is complete and verified now."
|
|
26
|
+
),
|
|
27
|
+
false,
|
|
28
|
+
"resolved historical pause was mistaken for current unfinished work"
|
|
29
|
+
);
|
|
30
|
+
assert.equal(
|
|
31
|
+
finishResultClaimsIncompleteWork(
|
|
32
|
+
"The earlier step was paused and the report is still incomplete."
|
|
33
|
+
),
|
|
34
|
+
true,
|
|
35
|
+
"historical-pause normalization hid genuinely incomplete current work"
|
|
36
|
+
);
|
|
37
|
+
assert.equal(
|
|
38
|
+
finishResultClaimsIncompleteWork(
|
|
39
|
+
"The earlier report was complete, but the current task is unfinished."
|
|
40
|
+
),
|
|
41
|
+
true,
|
|
42
|
+
"historical wording hid an unrelated current unfinished task"
|
|
43
|
+
);
|
|
44
|
+
|
|
15
45
|
function assistant(content, toolCalls = []) {
|
|
16
46
|
return {
|
|
17
47
|
choices: [
|
|
@@ -9,9 +9,8 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
10
|
const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-web-port-fallback-"));
|
|
11
11
|
const host = "127.0.0.1";
|
|
12
|
-
const occupiedPort = 43100 + Math.floor(Math.random() * 1200);
|
|
13
12
|
|
|
14
|
-
function listenOccupier(port) {
|
|
13
|
+
function listenOccupier(port = 0) {
|
|
15
14
|
return new Promise((resolve, reject) => {
|
|
16
15
|
const server = net.createServer();
|
|
17
16
|
server.once("error", reject);
|
|
@@ -49,7 +48,15 @@ async function waitForHealth(port, child, stdout, stderr) {
|
|
|
49
48
|
throw new Error(`web fallback health failed. stdout=${stdout()} stderr=${stderr()}`);
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
// Let the kernel allocate an actually free port and retain the listener. A
|
|
52
|
+
// random high port can already belong to another project on a shared runner,
|
|
53
|
+
// which made this fallback test itself flaky before the product was exercised.
|
|
54
|
+
const occupier = await listenOccupier();
|
|
55
|
+
const occupiedAddress = occupier.address();
|
|
56
|
+
const occupiedPort = typeof occupiedAddress === "object" && occupiedAddress
|
|
57
|
+
? Number(occupiedAddress.port)
|
|
58
|
+
: 0;
|
|
59
|
+
if (!occupiedPort) throw new Error("failed to reserve the occupied test port");
|
|
53
60
|
let stdout = "";
|
|
54
61
|
let stderr = "";
|
|
55
62
|
const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "web", "--port", String(occupiedPort), "--host", host], {
|
package/src/command-policy.js
CHANGED
|
@@ -78,6 +78,47 @@ function stripBenignRedirections(command = "") {
|
|
|
78
78
|
.trim();
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function isReadOnlyPrintfCommand(command = "") {
|
|
82
|
+
const normalized = stripBenignRedirections(command);
|
|
83
|
+
if (!/^printf(?:\s|$)/.test(normalized) || hasActiveShellExpansion(normalized)) return false;
|
|
84
|
+
const tokens = tokenizeShellWords(normalized);
|
|
85
|
+
if (!tokens.length || tokens[0] !== "printf") return false;
|
|
86
|
+
// Bash's `printf -v name ...` mutates shell state and can influence a later
|
|
87
|
+
// command. Ordinary printf writes only to stdout and is safe once expansion
|
|
88
|
+
// and redirection checks have passed.
|
|
89
|
+
return !tokens.slice(1).some((token) => token === "-v" || /^-v[A-Za-z_]/.test(token));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isReadOnlyTrDeleteFilter(command = "") {
|
|
93
|
+
const normalized = stripBenignRedirections(command);
|
|
94
|
+
if (hasActiveShellExpansion(normalized)) return false;
|
|
95
|
+
const tokens = tokenizeShellWords(normalized);
|
|
96
|
+
return tokens.length === 3 && tokens[0] === "tr" && tokens[1] === "-d";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isReadOnlyUniqFilter(command = "") {
|
|
100
|
+
const normalized = stripBenignRedirections(command);
|
|
101
|
+
if (hasActiveShellExpansion(normalized)) return false;
|
|
102
|
+
const tokens = tokenizeShellWords(normalized);
|
|
103
|
+
if (!tokens.length || tokens[0] !== "uniq") return false;
|
|
104
|
+
// Restrict uniq to stdin-only display/count options. Positional operands are
|
|
105
|
+
// intentionally rejected because a second operand is an output file.
|
|
106
|
+
return tokens.slice(1).every((token) =>
|
|
107
|
+
/^(?:-[cdui]+|--(?:count|repeated|unique|ignore-case))$/.test(token)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isReadOnlySha256Command(command = "") {
|
|
112
|
+
const normalized = stripBenignRedirections(command);
|
|
113
|
+
if (hasActiveShellExpansion(normalized)) return false;
|
|
114
|
+
const tokens = tokenizeShellWords(normalized);
|
|
115
|
+
if (tokens[0] !== "sha256sum" || tokens.length < 2) return false;
|
|
116
|
+
const paths = tokens.slice(1).filter((token) => token !== "--");
|
|
117
|
+
return paths.length > 0 && paths.every((token) =>
|
|
118
|
+
!token.startsWith("-") && READ_ONLY_FOR_LOOP_LITERAL_PATTERN.test(token)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
81
122
|
function isReadOnlyFindCommand(command = "") {
|
|
82
123
|
const normalized = stripBenignRedirections(command);
|
|
83
124
|
if (!/^find\s+/.test(normalized)) return false;
|
|
@@ -1359,6 +1400,10 @@ function classifySimpleCommand(normalized) {
|
|
|
1359
1400
|
|
|
1360
1401
|
if (
|
|
1361
1402
|
matchAny(READ_ONLY_PATTERNS, commandForPatternMatching) ||
|
|
1403
|
+
isReadOnlyPrintfCommand(commandForPatternMatching) ||
|
|
1404
|
+
isReadOnlyTrDeleteFilter(commandForPatternMatching) ||
|
|
1405
|
+
isReadOnlyUniqFilter(commandForPatternMatching) ||
|
|
1406
|
+
isReadOnlySha256Command(commandForPatternMatching) ||
|
|
1362
1407
|
isReadOnlyFindCommand(normalized) ||
|
|
1363
1408
|
(!hasActiveShellExpansion(benignRedirectCommand) && isReadOnlyShellCondition(benignRedirectCommand))
|
|
1364
1409
|
) {
|
|
@@ -1711,6 +1756,23 @@ function loopBodyHasOnlyBoundedReadOnlyExpansions(bodySource = "") {
|
|
|
1711
1756
|
function isBoundedReadOnlyScalarCommand(command = "") {
|
|
1712
1757
|
const normalized = stripBenignRedirections(command);
|
|
1713
1758
|
if (!normalized || hasActiveShellExpansion(normalized)) return false;
|
|
1759
|
+
const pipeline = splitTopLevelPipeline(normalized);
|
|
1760
|
+
if (pipeline) {
|
|
1761
|
+
if (pipeline.length > 4) return false;
|
|
1762
|
+
const classifications = pipeline.map((part) => classifySimpleCommand(part));
|
|
1763
|
+
if (classifications.some(
|
|
1764
|
+
(classification) => classification.category !== "read-only" || classification.writesWorkspace
|
|
1765
|
+
)) {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
const producers = [...pipeline];
|
|
1769
|
+
if (isReadOnlyTrDeleteFilter(producers.at(-1))) producers.pop();
|
|
1770
|
+
const finalProducer = stripBenignRedirections(producers.at(-1) || "");
|
|
1771
|
+
const finalTokens = tokenizeShellWords(finalProducer);
|
|
1772
|
+
return finalTokens[0] === "wc" &&
|
|
1773
|
+
finalTokens.length === 2 &&
|
|
1774
|
+
["-l", "--lines"].includes(finalTokens[1]);
|
|
1775
|
+
}
|
|
1714
1776
|
const tokens = tokenizeShellWords(normalized);
|
|
1715
1777
|
if (!tokens.length) return false;
|
|
1716
1778
|
if (["grep", "rg"].includes(tokens[0])) {
|
|
@@ -1721,17 +1783,109 @@ function isBoundedReadOnlyScalarCommand(command = "") {
|
|
|
1721
1783
|
return tokens[0] === "wc" && tokens.slice(1).includes("-l");
|
|
1722
1784
|
}
|
|
1723
1785
|
|
|
1786
|
+
function parseReadOnlyLoopBodySequence(value = "") {
|
|
1787
|
+
const text = String(value || "");
|
|
1788
|
+
const commands = [];
|
|
1789
|
+
const separators = [];
|
|
1790
|
+
let current = "";
|
|
1791
|
+
let quote = "";
|
|
1792
|
+
let escaped = false;
|
|
1793
|
+
let substitutionDepth = 0;
|
|
1794
|
+
|
|
1795
|
+
const push = (separator) => {
|
|
1796
|
+
const command = current.trim();
|
|
1797
|
+
if (!command) return false;
|
|
1798
|
+
commands.push(command);
|
|
1799
|
+
separators.push(separator);
|
|
1800
|
+
current = "";
|
|
1801
|
+
return true;
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1805
|
+
const char = text[index];
|
|
1806
|
+
if (quote === "'") {
|
|
1807
|
+
current += char;
|
|
1808
|
+
if (char === "'") quote = "";
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
if (escaped) {
|
|
1812
|
+
current += char;
|
|
1813
|
+
escaped = false;
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
if (char === "\\") {
|
|
1817
|
+
current += char;
|
|
1818
|
+
escaped = true;
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
if (char === '"') {
|
|
1822
|
+
quote = quote === '"' ? "" : '"';
|
|
1823
|
+
current += char;
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
if (!quote && char === "'") {
|
|
1827
|
+
quote = "'";
|
|
1828
|
+
current += char;
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
if (char === "$" && text[index + 1] === "(") {
|
|
1832
|
+
substitutionDepth += 1;
|
|
1833
|
+
current += "$(";
|
|
1834
|
+
index += 1;
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
if (!quote && char === ")" && substitutionDepth > 0) {
|
|
1838
|
+
substitutionDepth -= 1;
|
|
1839
|
+
current += char;
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
if (!quote && substitutionDepth === 0) {
|
|
1843
|
+
const pair = text.slice(index, index + 2);
|
|
1844
|
+
if (pair === "&&" || pair === "||") {
|
|
1845
|
+
if (!push(pair)) return null;
|
|
1846
|
+
index += 1;
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
if (char === ";" || char === "\n") {
|
|
1850
|
+
if (current.trim() && !push(char === "\n" ? "newline" : char)) return null;
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
current += char;
|
|
1855
|
+
}
|
|
1856
|
+
if (quote || escaped || substitutionDepth !== 0) return null;
|
|
1857
|
+
if (current.trim()) commands.push(current.trim());
|
|
1858
|
+
if (!commands.length || separators.length >= commands.length) return null;
|
|
1859
|
+
return { commands, separators };
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function commandHasUnbalancedSubstitution(value = "") {
|
|
1863
|
+
const text = String(value || "");
|
|
1864
|
+
let depth = 0;
|
|
1865
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1866
|
+
if (text[index] === "$" && text[index + 1] === "(") {
|
|
1867
|
+
depth += 1;
|
|
1868
|
+
index += 1;
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
if (text[index] === ")" && depth > 0) depth -= 1;
|
|
1872
|
+
}
|
|
1873
|
+
return depth !== 0;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1724
1876
|
function sanitizeReadOnlyLoopAssignments(bodySource = "") {
|
|
1725
1877
|
const text = String(bodySource || "").trim();
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1878
|
+
const standardSequence = parseTopLevelShellSequence(text);
|
|
1879
|
+
const standardIsUsable =
|
|
1880
|
+
standardSequence.commands.length > 0 &&
|
|
1881
|
+
!standardSequence.openQuote &&
|
|
1882
|
+
!standardSequence.trailingEscape &&
|
|
1883
|
+
!standardSequence.trailingSeparator &&
|
|
1884
|
+
!standardSequence.commands.some(commandHasUnbalancedSubstitution);
|
|
1885
|
+
const sequence = standardIsUsable
|
|
1886
|
+
? standardSequence
|
|
1887
|
+
: parseReadOnlyLoopBodySequence(text);
|
|
1888
|
+
if (!sequence) return null;
|
|
1735
1889
|
|
|
1736
1890
|
const assigned = new Map();
|
|
1737
1891
|
const commands = [];
|
|
@@ -1741,7 +1895,7 @@ function sanitizeReadOnlyLoopAssignments(bodySource = "") {
|
|
|
1741
1895
|
const escapedName = shellIdentifierPattern(name);
|
|
1742
1896
|
const reference = new RegExp(`\\$(?:\\{${escapedName}\\}|${escapedName}(?![A-Za-z0-9_]))`, "g");
|
|
1743
1897
|
if (!reference.test(command)) continue;
|
|
1744
|
-
if (!/^echo\s+/.test(command)) return null;
|
|
1898
|
+
if (!/^(?:echo|printf)\s+/.test(command)) return null;
|
|
1745
1899
|
command = command.replace(reference, `AGINTI_READ_VALUE_${name}`);
|
|
1746
1900
|
state.used = true;
|
|
1747
1901
|
}
|
|
@@ -1780,11 +1934,161 @@ function sanitizeReadOnlyLoopAssignments(bodySource = "") {
|
|
|
1780
1934
|
|
|
1781
1935
|
let sanitized = commands[0];
|
|
1782
1936
|
for (let index = 1; index < commands.length; index += 1) {
|
|
1783
|
-
|
|
1937
|
+
const separator = sequence.separators[index - 1] === "newline"
|
|
1938
|
+
? "\n"
|
|
1939
|
+
: sequence.separators[index - 1] || ";";
|
|
1940
|
+
sanitized += separator === "\n"
|
|
1941
|
+
? `\n${commands[index]}`
|
|
1942
|
+
: ` ${separator} ${commands[index]}`;
|
|
1784
1943
|
}
|
|
1785
1944
|
return sanitized;
|
|
1786
1945
|
}
|
|
1787
1946
|
|
|
1947
|
+
const READ_ONLY_LITERAL_ASSIGNMENT_MAX = 8;
|
|
1948
|
+
|
|
1949
|
+
function boundedLiteralAssignment(command = "") {
|
|
1950
|
+
const source = String(command || "").trim();
|
|
1951
|
+
if (!source || hasActiveShellExpansion(source)) return null;
|
|
1952
|
+
const tokens = tokenizeShellWords(source);
|
|
1953
|
+
if (tokens.length !== 1) return null;
|
|
1954
|
+
const match = tokens[0].match(/^([A-Za-z_][A-Za-z0-9_]*)=([\s\S]+)$/);
|
|
1955
|
+
if (!match) return null;
|
|
1956
|
+
const [, name, value] = match;
|
|
1957
|
+
if (
|
|
1958
|
+
value.startsWith("-") ||
|
|
1959
|
+
!READ_ONLY_FOR_LOOP_LITERAL_PATTERN.test(value) ||
|
|
1960
|
+
/[\r\n]/.test(value)
|
|
1961
|
+
) {
|
|
1962
|
+
return null;
|
|
1963
|
+
}
|
|
1964
|
+
return { name, value };
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function replaceBoundedLiteralReferences(source = "", name = "", value = "") {
|
|
1968
|
+
const text = String(source || "");
|
|
1969
|
+
let result = "";
|
|
1970
|
+
let quote = "";
|
|
1971
|
+
let escaped = false;
|
|
1972
|
+
let replacements = 0;
|
|
1973
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1974
|
+
const char = text[index];
|
|
1975
|
+
if (quote === "'") {
|
|
1976
|
+
result += char;
|
|
1977
|
+
if (char === "'") quote = "";
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
if (escaped) {
|
|
1981
|
+
result += char;
|
|
1982
|
+
escaped = false;
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
if (char === "\\") {
|
|
1986
|
+
result += char;
|
|
1987
|
+
escaped = true;
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
if (char === '"') {
|
|
1991
|
+
quote = quote === '"' ? "" : '"';
|
|
1992
|
+
result += char;
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
if (!quote && char === "'") {
|
|
1996
|
+
quote = "'";
|
|
1997
|
+
result += char;
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
if (char !== "$") {
|
|
2001
|
+
result += char;
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
const braced = text.startsWith(`\${${name}}`, index);
|
|
2005
|
+
const bare = text.startsWith(`$${name}`, index) &&
|
|
2006
|
+
!/[A-Za-z0-9_]/.test(text[index + name.length + 1] || "");
|
|
2007
|
+
if (!braced && !bare) {
|
|
2008
|
+
result += char;
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
result += quote === '"' || !/\s/.test(value) ? value : `'${value}'`;
|
|
2012
|
+
replacements += 1;
|
|
2013
|
+
index += braced ? name.length + 2 : name.length;
|
|
2014
|
+
}
|
|
2015
|
+
return { text: result, replacements };
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function joinParsedShellSequence(commands = [], separators = []) {
|
|
2019
|
+
let result = commands[0] || "";
|
|
2020
|
+
for (let index = 1; index < commands.length; index += 1) {
|
|
2021
|
+
const separator = separators[index - 1] === "newline"
|
|
2022
|
+
? "\n"
|
|
2023
|
+
: separators[index - 1] || ";";
|
|
2024
|
+
result += separator === "\n"
|
|
2025
|
+
? `\n${commands[index]}`
|
|
2026
|
+
: ` ${separator} ${commands[index]}`;
|
|
2027
|
+
}
|
|
2028
|
+
return result;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function sanitizeReadOnlyCompoundLiteralAssignments({
|
|
2032
|
+
prefixSource = "",
|
|
2033
|
+
loopSource = "",
|
|
2034
|
+
suffixSource = "",
|
|
2035
|
+
loopVariable = "",
|
|
2036
|
+
} = {}) {
|
|
2037
|
+
const parsed = parseTopLevelShellSequence(prefixSource);
|
|
2038
|
+
if (
|
|
2039
|
+
!parsed.commands.length ||
|
|
2040
|
+
parsed.openQuote ||
|
|
2041
|
+
parsed.trailingEscape ||
|
|
2042
|
+
parsed.trailingSeparator
|
|
2043
|
+
) {
|
|
2044
|
+
return null;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
const assignments = new Map();
|
|
2048
|
+
const commands = [];
|
|
2049
|
+
for (const sourceCommand of parsed.commands) {
|
|
2050
|
+
const assignment = boundedLiteralAssignment(sourceCommand);
|
|
2051
|
+
if (assignment) {
|
|
2052
|
+
if (
|
|
2053
|
+
assignments.has(assignment.name) ||
|
|
2054
|
+
assignment.name === loopVariable ||
|
|
2055
|
+
assignments.size >= READ_ONLY_LITERAL_ASSIGNMENT_MAX
|
|
2056
|
+
) {
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
assignments.set(assignment.name, { value: assignment.value, used: false });
|
|
2060
|
+
commands.push("true");
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
let command = sourceCommand;
|
|
2064
|
+
for (const [name, state] of assignments.entries()) {
|
|
2065
|
+
const replaced = replaceBoundedLiteralReferences(command, name, state.value);
|
|
2066
|
+
command = replaced.text;
|
|
2067
|
+
state.used ||= replaced.replacements > 0;
|
|
2068
|
+
}
|
|
2069
|
+
commands.push(command);
|
|
2070
|
+
}
|
|
2071
|
+
if (!assignments.size) {
|
|
2072
|
+
return { prefixSource, loopSource, suffixSource };
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
let sanitizedLoop = loopSource;
|
|
2076
|
+
let sanitizedSuffix = suffixSource;
|
|
2077
|
+
for (const [name, state] of assignments.entries()) {
|
|
2078
|
+
const loopReplacement = replaceBoundedLiteralReferences(sanitizedLoop, name, state.value);
|
|
2079
|
+
sanitizedLoop = loopReplacement.text;
|
|
2080
|
+
const suffixReplacement = replaceBoundedLiteralReferences(sanitizedSuffix, name, state.value);
|
|
2081
|
+
sanitizedSuffix = suffixReplacement.text;
|
|
2082
|
+
state.used ||= loopReplacement.replacements > 0 || suffixReplacement.replacements > 0;
|
|
2083
|
+
}
|
|
2084
|
+
if ([...assignments.values()].some((state) => !state.used)) return null;
|
|
2085
|
+
return {
|
|
2086
|
+
prefixSource: joinParsedShellSequence(commands, parsed.separators),
|
|
2087
|
+
loopSource: sanitizedLoop,
|
|
2088
|
+
suffixSource: sanitizedSuffix,
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
1788
2092
|
function classifyReadOnlyForLoop(normalized) {
|
|
1789
2093
|
const text = String(normalized || "").trim();
|
|
1790
2094
|
if (!/^for\s+/.test(text)) return null;
|
|
@@ -1801,7 +2105,11 @@ function classifyReadOnlyForLoop(normalized) {
|
|
|
1801
2105
|
if (hasActiveShellExpansion(itemSource) || hasActiveShellCommandSubstitution(itemSource)) {
|
|
1802
2106
|
return broadForLoopClassification(text, "the item list is dynamic");
|
|
1803
2107
|
}
|
|
1804
|
-
|
|
2108
|
+
// A model commonly formats a finite literal list with shell line
|
|
2109
|
+
// continuations. Removing only the exact backslash-newline token preserves
|
|
2110
|
+
// the same static list without admitting arbitrary escapes or expansion.
|
|
2111
|
+
const normalizedItemSource = itemSource.replace(/\\\r?\n/g, " ");
|
|
2112
|
+
const items = tokenizeShellWords(normalizedItemSource);
|
|
1805
2113
|
if (!items.length || items.length > READ_ONLY_FOR_LOOP_MAX_ITEMS) {
|
|
1806
2114
|
return broadForLoopClassification(text, "the item list is empty or unbounded");
|
|
1807
2115
|
}
|
|
@@ -1862,13 +2170,27 @@ function classifyReadOnlyCompoundSequence(normalized) {
|
|
|
1862
2170
|
}
|
|
1863
2171
|
if (forIndex < 0) return null;
|
|
1864
2172
|
|
|
1865
|
-
|
|
2173
|
+
let prefixSource = trimShellListBoundary(text.slice(0, forIndex));
|
|
1866
2174
|
const doneIndex = findUnquotedShellWord(text, "done", forIndex + 3);
|
|
1867
2175
|
if (doneIndex < 0) return null;
|
|
1868
|
-
|
|
1869
|
-
|
|
2176
|
+
let loopSource = text.slice(forIndex, doneIndex + 4).trim();
|
|
2177
|
+
let suffixSource = trimShellListBoundary(text.slice(doneIndex + 4));
|
|
1870
2178
|
if (!prefixSource && !suffixSource) return null;
|
|
1871
2179
|
|
|
2180
|
+
const loopVariable = loopSource.match(/^for[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]+in\b/)?.[1] || "";
|
|
2181
|
+
if (prefixSource) {
|
|
2182
|
+
const sanitized = sanitizeReadOnlyCompoundLiteralAssignments({
|
|
2183
|
+
prefixSource,
|
|
2184
|
+
loopSource,
|
|
2185
|
+
suffixSource,
|
|
2186
|
+
loopVariable,
|
|
2187
|
+
});
|
|
2188
|
+
if (!sanitized) {
|
|
2189
|
+
return broadForLoopClassification(text, "the prelude contains an unsafe or unused shell assignment");
|
|
2190
|
+
}
|
|
2191
|
+
({ prefixSource, loopSource, suffixSource } = sanitized);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
1872
2194
|
for (const [label, source] of [["prelude", prefixSource], ["suffix", suffixSource]]) {
|
|
1873
2195
|
if (!source) continue;
|
|
1874
2196
|
const classification = classifyReadOnlyCommandList(source);
|
package/src/scs-evidence.js
CHANGED
|
@@ -2952,6 +2952,14 @@ export function finishResultClaimsBlocker(result = "") {
|
|
|
2952
2952
|
|
|
2953
2953
|
export function finishResultClaimsIncompleteWork(result = "") {
|
|
2954
2954
|
const text = String(result || "")
|
|
2955
|
+
.replace(
|
|
2956
|
+
/\b(?:earlier|previous|prior|former)\s+(?:step|attempt|run|verification|check|phase|command|process|audit)\s+(?:was|were|had\s+been)\s+(?:paused|pending|incomplete|unfinished)\b/gi,
|
|
2957
|
+
""
|
|
2958
|
+
)
|
|
2959
|
+
.replace(
|
|
2960
|
+
/\b(?:(?:was|were|had\s+been)\s+)?(?:previously|formerly|earlier|already)\s+(?:paused|pending|incomplete|unfinished)\b/gi,
|
|
2961
|
+
""
|
|
2962
|
+
)
|
|
2955
2963
|
.replace(/\b(?:not|never)\s+(?:paused|pending|incomplete|unfinished)\b/gi, "")
|
|
2956
2964
|
.replace(/\b(?:no|without)\s+(?:remaining|pending|unfinished)\s+(?:work|steps?|tasks?|actions?)\b/gi, "");
|
|
2957
2965
|
const explicitIncompleteState =
|