@lazyingart/agintiflow 0.20.314 → 0.20.316
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.
- package/package.json +1 -1
- package/scripts/smoke-coding-tools.js +1 -1
- package/scripts/smoke-dynamic-step-budget.js +149 -2
- package/src/agent-runner.js +81 -11
- package/src/command-policy.js +65 -3
- package/src/scs-evidence.js +41 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.316",
|
|
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",
|
|
@@ -1431,7 +1431,7 @@ try {
|
|
|
1431
1431
|
"host workspace chmod sequence should remain categorized as permission-change"
|
|
1432
1432
|
);
|
|
1433
1433
|
const androidReadonlyProbePolicy = evaluateCommandPolicy(
|
|
1434
|
-
'test -x /usr/bin/gradle && echo "GRADLE_OK" ; test -x /usr/bin/java && /usr/bin/java -version 2>&1 | head -1; adb devices; emulator -list-avds; find android-app -type f | sort',
|
|
1434
|
+
'test -x /usr/bin/gradle && echo "GRADLE_OK" ; test -x /usr/bin/java && /usr/bin/java -version 2>&1 | head -1; adb devices; emulator -list-avds; find android-app -maxdepth 16 -type f | sort',
|
|
1435
1435
|
hostWorkspacePolicy
|
|
1436
1436
|
);
|
|
1437
1437
|
assert(androidReadonlyProbePolicy.allowed, "Android host read-only probes should not require full-host destructive access");
|
|
@@ -4556,6 +4556,36 @@ try {
|
|
|
4556
4556
|
failedSpacedExitProbeResult.projectTest?.explicitExitStatus === 1,
|
|
4557
4557
|
"a spaced nonzero test-status wrapper was recorded as successful generic evidence"
|
|
4558
4558
|
);
|
|
4559
|
+
const pipedUnittestCommand =
|
|
4560
|
+
"python3 -m unittest discover -s tests -p 'test_wechat_document_reader.py' -v 2>&1 | tail -14; echo \"EXIT=${PIPESTATUS[0]}\"";
|
|
4561
|
+
assert(
|
|
4562
|
+
parseNonMutatingExitStatusWrapper(pipedUnittestCommand)?.command ===
|
|
4563
|
+
"python3 -m unittest discover -s tests -p 'test_wechat_document_reader.py' -v 2>&1" &&
|
|
4564
|
+
isSubstantiveTestCommand(pipedUnittestCommand),
|
|
4565
|
+
"the exact bounded PIPESTATUS unittest wrapper lost its test identity"
|
|
4566
|
+
);
|
|
4567
|
+
for (const [status, expectedPassed] of [[0, true], [1, false]]) {
|
|
4568
|
+
const state = { meta: { goalContract: { revision: 1 } } };
|
|
4569
|
+
const result = {
|
|
4570
|
+
toolName: "run_command",
|
|
4571
|
+
ok: true,
|
|
4572
|
+
exitCode: 0,
|
|
4573
|
+
args: { command: pipedUnittestCommand },
|
|
4574
|
+
stdout: `Ran 7 tests in 0.01s\n${status === 0 ? "OK" : "FAILED (failures=1)"}\nEXIT=${status}\n`,
|
|
4575
|
+
stderr: "",
|
|
4576
|
+
commandPolicy: classifyCommand(pipedUnittestCommand),
|
|
4577
|
+
};
|
|
4578
|
+
recordProjectVerificationOutcome(state, result, {
|
|
4579
|
+
commandCwd: workspace,
|
|
4580
|
+
taskProfile: "qa",
|
|
4581
|
+
});
|
|
4582
|
+
assert(
|
|
4583
|
+
result.projectTest?.passed === expectedPassed &&
|
|
4584
|
+
result.projectTest?.explicitExitStatus === status &&
|
|
4585
|
+
state.meta.projectVerification?.mutationRevision === 0,
|
|
4586
|
+
`a PIPESTATUS unittest wrapper was not recorded at the correct status/revision: ${status}`
|
|
4587
|
+
);
|
|
4588
|
+
}
|
|
4559
4589
|
for (const [command, output, expectedRevision] of [
|
|
4560
4590
|
['npm test; printf "EXIT_CODE=%d\\n" "$?"', "EXIT_CODE=0\n", 0],
|
|
4561
4591
|
['git push; echo "EXIT=$?"', "EXIT=0\n", 0],
|
|
@@ -4579,6 +4609,50 @@ try {
|
|
|
4579
4609
|
`a status wrapper changed the inner command's mutation identity: ${command}`
|
|
4580
4610
|
);
|
|
4581
4611
|
}
|
|
4612
|
+
const boundedFindCommand =
|
|
4613
|
+
`cd ${workspace} && echo "=== CANDIDATES ===" && ` +
|
|
4614
|
+
"find . -maxdepth 4 \\( -name '*.py' -o -name '*.md' \\) -not -path './output/*' 2>/dev/null | sort; echo DONE";
|
|
4615
|
+
const boundedFindPolicy = evaluateCommandPolicy(boundedFindCommand, {
|
|
4616
|
+
commandCwd: workspace,
|
|
4617
|
+
allowShellTool: true,
|
|
4618
|
+
allowDestructive: false,
|
|
4619
|
+
sandboxMode: "host",
|
|
4620
|
+
});
|
|
4621
|
+
assert(
|
|
4622
|
+
boundedFindPolicy.allowed === true &&
|
|
4623
|
+
boundedFindPolicy.category === "read-only" &&
|
|
4624
|
+
boundedFindPolicy.writesWorkspace === false,
|
|
4625
|
+
"the exact bounded read-only find sequence was classified as a project mutation"
|
|
4626
|
+
);
|
|
4627
|
+
const boundedFindState = { meta: { goalContract: { revision: 1 } } };
|
|
4628
|
+
const boundedFindResult = {
|
|
4629
|
+
toolName: "run_command",
|
|
4630
|
+
ok: true,
|
|
4631
|
+
exitCode: 0,
|
|
4632
|
+
args: { command: boundedFindCommand },
|
|
4633
|
+
stdout: "./tests/test_wechat_document_reader.py\nDONE\n",
|
|
4634
|
+
stderr: "",
|
|
4635
|
+
commandPolicy: boundedFindPolicy,
|
|
4636
|
+
};
|
|
4637
|
+
recordProjectVerificationOutcome(boundedFindState, boundedFindResult, {
|
|
4638
|
+
commandCwd: workspace,
|
|
4639
|
+
taskProfile: "qa",
|
|
4640
|
+
});
|
|
4641
|
+
assert(
|
|
4642
|
+
boundedFindState.meta.projectVerification?.mutationRevision === 0,
|
|
4643
|
+
"a successful bounded read-only find fabricated a source revision"
|
|
4644
|
+
);
|
|
4645
|
+
for (const command of [
|
|
4646
|
+
"find . -maxdepth 4 -name '*.py' -delete",
|
|
4647
|
+
"find . -maxdepth 4 -name '*.py' -exec touch {} \\;",
|
|
4648
|
+
]) {
|
|
4649
|
+
const classification = classifyCommand(command);
|
|
4650
|
+
assert(
|
|
4651
|
+
classification.writesWorkspace === true &&
|
|
4652
|
+
["destructive", "general-shell"].includes(classification.category),
|
|
4653
|
+
`a write-capable find form was accepted as read-only: ${command}`
|
|
4654
|
+
);
|
|
4655
|
+
}
|
|
4582
4656
|
const readOnlyValidatorState = { meta: { goalContract: { revision: 1 } } };
|
|
4583
4657
|
const readOnlyValidatorResult = {
|
|
4584
4658
|
toolName: "run_command",
|
|
@@ -6030,13 +6104,86 @@ try {
|
|
|
6030
6104
|
sandboxMode: "host",
|
|
6031
6105
|
}
|
|
6032
6106
|
);
|
|
6107
|
+
const scopedCompoundTestLogCommand = {
|
|
6108
|
+
toolName: "run_command",
|
|
6109
|
+
ok: true,
|
|
6110
|
+
exitCode: 0,
|
|
6111
|
+
args: {
|
|
6112
|
+
command: [
|
|
6113
|
+
`cd ${JSON.stringify(workspace)}`,
|
|
6114
|
+
`LOG=${JSON.stringify(path.join(scopedTaskRoot, "document-reader-test.log"))}`,
|
|
6115
|
+
'echo "RUN_MARKER=now" > "$LOG"',
|
|
6116
|
+
'python3 -m unittest discover -s tests -p \'test_reader.py\' -v >> "$LOG" 2>&1',
|
|
6117
|
+
'echo "EXIT=0" >> "$LOG"',
|
|
6118
|
+
'cat "$LOG"',
|
|
6119
|
+
].join(" && "),
|
|
6120
|
+
},
|
|
6121
|
+
projectMutationPaths: [],
|
|
6122
|
+
stdout: "Ran 7 tests\nOK\nEXIT=0\n",
|
|
6123
|
+
stderr: "",
|
|
6124
|
+
};
|
|
6125
|
+
recordProjectVerificationOutcome(
|
|
6126
|
+
scopedTaskState,
|
|
6127
|
+
scopedCompoundTestLogCommand,
|
|
6128
|
+
{
|
|
6129
|
+
commandCwd: workspace,
|
|
6130
|
+
taskProfile: "auto",
|
|
6131
|
+
allowShellTool: true,
|
|
6132
|
+
sandboxMode: "host",
|
|
6133
|
+
}
|
|
6134
|
+
);
|
|
6135
|
+
const nullRedirectValidationCommand = {
|
|
6136
|
+
toolName: "run_command",
|
|
6137
|
+
ok: true,
|
|
6138
|
+
exitCode: 0,
|
|
6139
|
+
args: {
|
|
6140
|
+
command: [
|
|
6141
|
+
`cd ${JSON.stringify(workspace)}`,
|
|
6142
|
+
`python3 -m json.tool ${JSON.stringify(
|
|
6143
|
+
path.join(scopedTaskRoot, "agent-result.json")
|
|
6144
|
+
)} > /dev/null`,
|
|
6145
|
+
'echo "JSON_VALID"',
|
|
6146
|
+
`grep -c '^## [0-9]' ${JSON.stringify(
|
|
6147
|
+
path.join(scopedTaskRoot, "source-intake-audit.md")
|
|
6148
|
+
)}`,
|
|
6149
|
+
].join(" && "),
|
|
6150
|
+
},
|
|
6151
|
+
projectMutationPaths: [],
|
|
6152
|
+
stdout: "JSON_VALID\n5\n",
|
|
6153
|
+
stderr: "",
|
|
6154
|
+
};
|
|
6155
|
+
recordProjectVerificationOutcome(
|
|
6156
|
+
scopedTaskState,
|
|
6157
|
+
nullRedirectValidationCommand,
|
|
6158
|
+
{
|
|
6159
|
+
commandCwd: workspace,
|
|
6160
|
+
taskProfile: "auto",
|
|
6161
|
+
allowShellTool: true,
|
|
6162
|
+
sandboxMode: "host",
|
|
6163
|
+
}
|
|
6164
|
+
);
|
|
6033
6165
|
assert(
|
|
6034
6166
|
requestedOutputRevision === 1 &&
|
|
6035
6167
|
scopedTaskState.meta.projectVerification?.mutationRevision === 1 &&
|
|
6036
6168
|
readOnlyArtifactValidation.readOnlyArtifactValidation === true &&
|
|
6037
6169
|
scopedManifestCommand.scopedTaskArtifactWrite === true &&
|
|
6038
|
-
pythonScopedManifestCommand.scopedTaskArtifactWrite === true
|
|
6039
|
-
|
|
6170
|
+
pythonScopedManifestCommand.scopedTaskArtifactWrite === true &&
|
|
6171
|
+
scopedCompoundTestLogCommand.scopedTaskArtifactWrite === true &&
|
|
6172
|
+
nullRedirectValidationCommand.readOnlyArtifactValidation === true,
|
|
6173
|
+
`task-scoped delivery bookkeeping invalidated requested artifact evidence: ${JSON.stringify({
|
|
6174
|
+
requestedOutputRevision,
|
|
6175
|
+
mutationRevision:
|
|
6176
|
+
scopedTaskState.meta.projectVerification?.mutationRevision,
|
|
6177
|
+
readOnlyArtifactValidation:
|
|
6178
|
+
readOnlyArtifactValidation.readOnlyArtifactValidation,
|
|
6179
|
+
scopedManifestCommand: scopedManifestCommand.scopedTaskArtifactWrite,
|
|
6180
|
+
pythonScopedManifestCommand:
|
|
6181
|
+
pythonScopedManifestCommand.scopedTaskArtifactWrite,
|
|
6182
|
+
scopedCompoundTestLogCommand:
|
|
6183
|
+
scopedCompoundTestLogCommand.scopedTaskArtifactWrite,
|
|
6184
|
+
nullRedirectValidationCommand:
|
|
6185
|
+
nullRedirectValidationCommand.readOnlyArtifactValidation,
|
|
6186
|
+
})}`
|
|
6040
6187
|
);
|
|
6041
6188
|
const mutatingInlinePython = {
|
|
6042
6189
|
toolName: "run_command",
|
package/src/agent-runner.js
CHANGED
|
@@ -5143,12 +5143,20 @@ function testRunRepresentsInvalidInvocation(testRun = {}) {
|
|
|
5143
5143
|
function explicitExitProbeStatus(command = "", result = {}) {
|
|
5144
5144
|
const normalizedCommand = normalizeProjectCommand(command);
|
|
5145
5145
|
const exitProbe = parseNonMutatingExitStatusWrapper(normalizedCommand);
|
|
5146
|
-
if (!exitProbe)
|
|
5146
|
+
if (!exitProbe) {
|
|
5147
|
+
return {
|
|
5148
|
+
present: false,
|
|
5149
|
+
status: null,
|
|
5150
|
+
command: normalizedCommand,
|
|
5151
|
+
wrappedCommand: normalizedCommand,
|
|
5152
|
+
};
|
|
5153
|
+
}
|
|
5147
5154
|
|
|
5148
5155
|
return {
|
|
5149
5156
|
present: true,
|
|
5150
5157
|
status: parseExplicitExitStatus(result.stdout || ""),
|
|
5151
5158
|
command: exitProbe.command,
|
|
5159
|
+
wrappedCommand: exitProbe.wrappedCommand || exitProbe.command,
|
|
5152
5160
|
};
|
|
5153
5161
|
}
|
|
5154
5162
|
|
|
@@ -6717,7 +6725,10 @@ export function recordProjectVerificationOutcome(state = {}, toolResult = {}, co
|
|
|
6717
6725
|
);
|
|
6718
6726
|
const exitProbe = explicitExitProbeStatus(command, toolResult);
|
|
6719
6727
|
const mutationCommand = normalizeProjectCommand(
|
|
6720
|
-
normalizeCommandForPolicy(
|
|
6728
|
+
normalizeCommandForPolicy(
|
|
6729
|
+
exitProbe.wrappedCommand || exitProbe.command || command,
|
|
6730
|
+
config
|
|
6731
|
+
)
|
|
6721
6732
|
);
|
|
6722
6733
|
// The result proves the command already executed. Classify its semantic
|
|
6723
6734
|
// mutation capability independently of whether the current policy would
|
|
@@ -12762,31 +12773,85 @@ function safePatchContextEvidencePath(value = "", state = {}, config = {}) {
|
|
|
12762
12773
|
function commandWritesOnlyEvidenceMatching(command = "", pathMatches = () => false) {
|
|
12763
12774
|
const normalized = String(command || "").trim();
|
|
12764
12775
|
if (!normalized) return false;
|
|
12776
|
+
const assignments = new Map();
|
|
12777
|
+
for (const match of normalized.matchAll(
|
|
12778
|
+
/(?:^|[\s;&|])([A-Za-z_]\w*)\s*=\s*("[^"\n]+"|'[^'\n]+'|[^\s;&|]+)/g
|
|
12779
|
+
)) {
|
|
12780
|
+
assignments.set(
|
|
12781
|
+
match[1],
|
|
12782
|
+
String(match[2] || "").replace(/^["']|["']$/g, "")
|
|
12783
|
+
);
|
|
12784
|
+
}
|
|
12785
|
+
const resolveTarget = (value = "") => {
|
|
12786
|
+
const target = String(value || "")
|
|
12787
|
+
.trim()
|
|
12788
|
+
.replace(/^["']|["']$/g, "");
|
|
12789
|
+
const variable = target.match(/^\$(?:\{([A-Za-z_]\w*)\}|([A-Za-z_]\w*))$/);
|
|
12790
|
+
return variable
|
|
12791
|
+
? String(assignments.get(variable[1] || variable[2]) || "")
|
|
12792
|
+
: target;
|
|
12793
|
+
};
|
|
12765
12794
|
const tokens = tokenizeShellWords(normalized);
|
|
12766
12795
|
if (tokens[0] === "tee") {
|
|
12767
12796
|
let index = 1;
|
|
12768
12797
|
if (["-a", "--append"].includes(tokens[index])) index += 1;
|
|
12769
12798
|
if (tokens[index] === "--") index += 1;
|
|
12770
|
-
const targets = tokens.slice(index);
|
|
12799
|
+
const targets = tokens.slice(index).map(resolveTarget);
|
|
12771
12800
|
return targets.length > 0 && targets.every(pathMatches);
|
|
12772
12801
|
}
|
|
12773
12802
|
if (tokens[0] === "mkdir") {
|
|
12774
|
-
const targets = tokens
|
|
12803
|
+
const targets = tokens
|
|
12804
|
+
.slice(1)
|
|
12805
|
+
.filter((token) => token !== "-p" && token !== "--")
|
|
12806
|
+
.map(resolveTarget);
|
|
12775
12807
|
return targets.length > 0 && targets.every(pathMatches);
|
|
12776
12808
|
}
|
|
12777
12809
|
|
|
12778
12810
|
let strippedEvidenceRedirect = false;
|
|
12811
|
+
let rejectedRedirect = false;
|
|
12779
12812
|
const withoutEvidenceRedirects = normalized.replace(
|
|
12780
12813
|
/(^|\s)(?:\d*>>?|&>>?)\s*("[^"]+"|'[^']+'|[^\s;&|]+)/g,
|
|
12781
12814
|
(match, prefix, target) => {
|
|
12782
|
-
|
|
12815
|
+
const resolvedTarget = resolveTarget(target);
|
|
12816
|
+
if (/^(?:&\d+|\/dev\/null)$/u.test(resolvedTarget)) return prefix;
|
|
12817
|
+
if (!resolvedTarget || !pathMatches(resolvedTarget)) {
|
|
12818
|
+
rejectedRedirect = true;
|
|
12819
|
+
return match;
|
|
12820
|
+
}
|
|
12783
12821
|
strippedEvidenceRedirect = true;
|
|
12784
12822
|
return prefix;
|
|
12785
12823
|
}
|
|
12786
12824
|
).trim();
|
|
12787
|
-
if (!strippedEvidenceRedirect || !withoutEvidenceRedirects)
|
|
12788
|
-
|
|
12789
|
-
|
|
12825
|
+
if (rejectedRedirect || !strippedEvidenceRedirect || !withoutEvidenceRedirects) {
|
|
12826
|
+
return false;
|
|
12827
|
+
}
|
|
12828
|
+
const sequence = parseTopLevelShellSequence(withoutEvidenceRedirects);
|
|
12829
|
+
if (sequence.openQuote || sequence.trailingEscape || sequence.trailingSeparator) {
|
|
12830
|
+
return false;
|
|
12831
|
+
}
|
|
12832
|
+
return sequence.commands.every((segment) => {
|
|
12833
|
+
const withoutAssignments = String(segment || "")
|
|
12834
|
+
.trim()
|
|
12835
|
+
.replace(
|
|
12836
|
+
/^(?:[A-Za-z_]\w*=(?:"[^"\n]*"|'[^'\n]*'|[^\s;&|]+)\s*)+/u,
|
|
12837
|
+
""
|
|
12838
|
+
)
|
|
12839
|
+
.trim();
|
|
12840
|
+
if (!withoutAssignments) return true;
|
|
12841
|
+
const commandTokens = tokenizeShellWords(withoutAssignments);
|
|
12842
|
+
if (
|
|
12843
|
+
["cat", "cd", "head", "ls", "stat", "tail", "wc"].includes(
|
|
12844
|
+
String(commandTokens[0] || "")
|
|
12845
|
+
)
|
|
12846
|
+
) {
|
|
12847
|
+
return true;
|
|
12848
|
+
}
|
|
12849
|
+
const underlyingPolicy = classifyCommand(withoutAssignments);
|
|
12850
|
+
return (
|
|
12851
|
+
underlyingPolicy.writesWorkspace !== true &&
|
|
12852
|
+
underlyingPolicy.mayMutateProject !== true
|
|
12853
|
+
);
|
|
12854
|
+
});
|
|
12790
12855
|
}
|
|
12791
12856
|
|
|
12792
12857
|
function commandWritesOnlyPrivateVerificationEvidence(command = "") {
|
|
@@ -12833,15 +12898,20 @@ function commandIsBoundedReadOnlyArtifactValidation(command = "") {
|
|
|
12833
12898
|
const normalized = String(command || "").trim();
|
|
12834
12899
|
if (!normalized) return false;
|
|
12835
12900
|
if (
|
|
12836
|
-
!/\bassert\b|\bjson\.(?:load|loads)\
|
|
12901
|
+
!/\bassert\b|\bjson\.(?:load|loads|tool)\b|\bJSON\.parse\s*\(|\bjq\b|\bsha256sum\b|\bmd5sum\b|\b(?:valid|pass|verified?)\b/i.test(
|
|
12837
12902
|
normalized
|
|
12838
12903
|
)
|
|
12839
12904
|
) {
|
|
12840
12905
|
return false;
|
|
12841
12906
|
}
|
|
12907
|
+
const withoutNullRedirects = normalized.replace(
|
|
12908
|
+
/(^|\s)(?:\d*>>?|&>>?)\s*(?:"\/dev\/null"|'\/dev\/null'|\/dev\/null)/g,
|
|
12909
|
+
"$1"
|
|
12910
|
+
);
|
|
12842
12911
|
// Heredoc input (`<<`) is compatible with read-only validation. Any output
|
|
12843
|
-
// redirection or known mutator keeps conservative project
|
|
12844
|
-
|
|
12912
|
+
// redirection outside /dev/null or known mutator keeps conservative project
|
|
12913
|
+
// revision tracking.
|
|
12914
|
+
if (/(^|[^<])>{1,2}(?!=)/m.test(withoutNullRedirects)) return false;
|
|
12845
12915
|
if (
|
|
12846
12916
|
/\b(?:cp|install|mkdir|mv|rm|rmdir|tee|touch|truncate)\b|\bsed\s+-i\b|\bperl\s+-pi\b/i.test(
|
|
12847
12917
|
normalized
|
package/src/command-policy.js
CHANGED
|
@@ -17,7 +17,6 @@ const READ_ONLY_PATTERNS = [
|
|
|
17
17
|
/^command\s+-v\s+[-\w.]+$/,
|
|
18
18
|
/^uname(?:\s+-a)?$/,
|
|
19
19
|
/^ls(?:\s+(?:"[^"\n]*"|'[^'\n]*'|[-\w./~*]+))*$/,
|
|
20
|
-
/^find(?:\s+[./~\w-]+)*(?:\s+-maxdepth\s+\d+)?(?:\s+-type\s+[fd])?$/,
|
|
21
20
|
/^rg(?:\s+.+)?$/,
|
|
22
21
|
/^grep(?:\s+.+)?$/,
|
|
23
22
|
/^pgrep(?:\s+.+)?$/,
|
|
@@ -143,8 +142,71 @@ function isReadOnlyFindCommand(command = "") {
|
|
|
143
142
|
if (!/^find\s+/.test(normalized)) return false;
|
|
144
143
|
if (/(^|\s)(-delete|-exec|-execdir|-ok|-okdir|-fprint|-fprintf|-fls)\b/.test(normalized)) return false;
|
|
145
144
|
const unquoted = stripQuotedSegments(normalized);
|
|
146
|
-
if (/[|<>;&`$]/.test(unquoted)) return false;
|
|
147
|
-
|
|
145
|
+
if (/[|<>;&`$]/.test(unquoted) || hasActiveShellExpansion(normalized)) return false;
|
|
146
|
+
|
|
147
|
+
const tokens = tokenizeShellWords(normalized);
|
|
148
|
+
if (tokens[0] !== "find" || tokens.length < 4) return false;
|
|
149
|
+
let index = 1;
|
|
150
|
+
let roots = 0;
|
|
151
|
+
while (index < tokens.length && !tokens[index].startsWith("-") && !["(", ")", "!"].includes(tokens[index])) {
|
|
152
|
+
const root = tokens[index];
|
|
153
|
+
if (!/^(?:\.{1,2}|\/workspace(?:\/[-\w./~*]+)?|~?(?:\/)?[-\w./*]+)$/.test(root)) return false;
|
|
154
|
+
roots += 1;
|
|
155
|
+
index += 1;
|
|
156
|
+
}
|
|
157
|
+
if (!roots) return false;
|
|
158
|
+
|
|
159
|
+
let maxDepth = null;
|
|
160
|
+
let parenthesisDepth = 0;
|
|
161
|
+
const patternPredicates = new Set([
|
|
162
|
+
"-name",
|
|
163
|
+
"-iname",
|
|
164
|
+
"-path",
|
|
165
|
+
"-ipath",
|
|
166
|
+
"-wholename",
|
|
167
|
+
"-iwholename",
|
|
168
|
+
]);
|
|
169
|
+
const operators = new Set(["!", "-not", "-o", "-or", "-a", "-and", "-true", "-false", "-empty", "-print", "-print0", "-quit"]);
|
|
170
|
+
while (index < tokens.length) {
|
|
171
|
+
const token = tokens[index];
|
|
172
|
+
if (token === "(") {
|
|
173
|
+
parenthesisDepth += 1;
|
|
174
|
+
index += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (token === ")") {
|
|
178
|
+
parenthesisDepth -= 1;
|
|
179
|
+
if (parenthesisDepth < 0) return false;
|
|
180
|
+
index += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (operators.has(token)) {
|
|
184
|
+
index += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const argument = tokens[index + 1];
|
|
188
|
+
if (!argument || /[\u0000-\u001f\u007f]/u.test(argument)) return false;
|
|
189
|
+
if (token === "-maxdepth" || token === "-mindepth") {
|
|
190
|
+
if (!/^\d+$/.test(argument)) return false;
|
|
191
|
+
const depth = Number(argument);
|
|
192
|
+
if (!Number.isSafeInteger(depth) || depth > 64) return false;
|
|
193
|
+
if (token === "-maxdepth") maxDepth = depth;
|
|
194
|
+
index += 2;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (token === "-type") {
|
|
198
|
+
if (!/^[bcdpflsD]$/.test(argument)) return false;
|
|
199
|
+
index += 2;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (patternPredicates.has(token)) {
|
|
203
|
+
if (argument.length > 1024) return false;
|
|
204
|
+
index += 2;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
return parenthesisDepth === 0 && maxDepth !== null;
|
|
148
210
|
}
|
|
149
211
|
|
|
150
212
|
function isUnboundedRecursiveGrep(command = "") {
|
package/src/scs-evidence.js
CHANGED
|
@@ -2227,10 +2227,51 @@ function normalizeProjectCommand(value = "") {
|
|
|
2227
2227
|
return canonicalizeShellCommand(value);
|
|
2228
2228
|
}
|
|
2229
2229
|
|
|
2230
|
+
function pipelineStatusCommand(value = "", pipelineIndex = 0) {
|
|
2231
|
+
const sequence = parseTopLevelShellSequence(value);
|
|
2232
|
+
if (
|
|
2233
|
+
sequence.openQuote ||
|
|
2234
|
+
sequence.trailingEscape ||
|
|
2235
|
+
sequence.trailingSeparator ||
|
|
2236
|
+
sequence.commands.length < 2 ||
|
|
2237
|
+
sequence.separators.at(-1) !== "|"
|
|
2238
|
+
) {
|
|
2239
|
+
return "";
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
let pipelineStart = sequence.commands.length - 1;
|
|
2243
|
+
while (pipelineStart > 0 && sequence.separators[pipelineStart - 1] === "|") {
|
|
2244
|
+
pipelineStart -= 1;
|
|
2245
|
+
}
|
|
2246
|
+
const commandIndex = pipelineStart + Number(pipelineIndex || 0);
|
|
2247
|
+
if (commandIndex < pipelineStart || commandIndex >= sequence.commands.length) return "";
|
|
2248
|
+
return normalizeProjectCommand(sequence.commands[commandIndex]);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2230
2251
|
export function parseNonMutatingExitStatusWrapper(value = "") {
|
|
2231
2252
|
const normalized = normalizeProjectCommand(value);
|
|
2232
2253
|
if (!normalized) return null;
|
|
2233
2254
|
const label = "(?:EXIT|STATUS|RESULT)(?:_CODE)?";
|
|
2255
|
+
const pipelinePattern = new RegExp(
|
|
2256
|
+
`^(?<pipelineCommand>[\\s\\S]+);\\s*echo\\s+(?:"(?<doubleLabel>${label})\\s*[:=]\\s*\\$\\{PIPESTATUS\\[0\\]\\}"|(?<bareLabel>${label})\\s*[:=]\\s*\\$\\{PIPESTATUS\\[0\\]\\})$`,
|
|
2257
|
+
"i"
|
|
2258
|
+
);
|
|
2259
|
+
const pipelineMatch = normalized.match(pipelinePattern);
|
|
2260
|
+
if (pipelineMatch?.groups?.pipelineCommand) {
|
|
2261
|
+
const wrappedCommand = normalizeProjectCommand(pipelineMatch.groups.pipelineCommand);
|
|
2262
|
+
const command = pipelineStatusCommand(wrappedCommand, 0);
|
|
2263
|
+
if (command) {
|
|
2264
|
+
return {
|
|
2265
|
+
command,
|
|
2266
|
+
wrappedCommand,
|
|
2267
|
+
label: String(
|
|
2268
|
+
pipelineMatch.groups.doubleLabel || pipelineMatch.groups.bareLabel || ""
|
|
2269
|
+
).toUpperCase(),
|
|
2270
|
+
statusSource: "pipeline",
|
|
2271
|
+
pipelineIndex: 0,
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2234
2275
|
const patterns = [
|
|
2235
2276
|
new RegExp(
|
|
2236
2277
|
`^(?<command>[\\s\\S]+);\\s*echo\\s+(?:\"(?<doubleLabel>${label})\\s*[:=]\\s*\\$\\?\"|'(?<singleLabel>${label})\\s*[:=]\\s*\\$\\?'|(?<bareLabel>${label})\\s*[:=]\\s*\\$\\?)$`,
|