@lazyingart/agintiflow 0.20.315 → 0.20.317
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 +266 -0
- package/src/agent-runner.js +255 -26
- 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.317",
|
|
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",
|
|
@@ -6058,6 +6132,164 @@ try {
|
|
|
6058
6132
|
sandboxMode: "host",
|
|
6059
6133
|
}
|
|
6060
6134
|
);
|
|
6135
|
+
const scopedRelativeTaskRoot = path.relative(workspace, scopedTaskRoot).replace(/\\/g, "/");
|
|
6136
|
+
const liveScopedGroupTestLogCommand = {
|
|
6137
|
+
toolName: "run_command",
|
|
6138
|
+
ok: true,
|
|
6139
|
+
exitCode: 0,
|
|
6140
|
+
args: {
|
|
6141
|
+
command:
|
|
6142
|
+
`cd ${JSON.stringify(workspace)} && ` +
|
|
6143
|
+
`TASK=${scopedRelativeTaskRoot} && LOG="$TASK/document-reader-test.log" && ` +
|
|
6144
|
+
`{ echo "=== focused test ==="; python3 -m unittest discover -s tests -p 'test_reader.py' -v; RC=$?; echo "EXIT=$RC"; } ` +
|
|
6145
|
+
`> "$LOG" 2>&1; tail -n 6 "$LOG"; echo "LOG_OK=$([ -s "$LOG" ] && echo yes || echo no)"`,
|
|
6146
|
+
},
|
|
6147
|
+
projectMutationPaths: [],
|
|
6148
|
+
stdout: "Ran 7 tests in 0.01s\nOK\nEXIT=0\nLOG_OK=yes\n",
|
|
6149
|
+
stderr: "",
|
|
6150
|
+
};
|
|
6151
|
+
recordProjectVerificationOutcome(
|
|
6152
|
+
scopedTaskState,
|
|
6153
|
+
liveScopedGroupTestLogCommand,
|
|
6154
|
+
{
|
|
6155
|
+
commandCwd: workspace,
|
|
6156
|
+
taskProfile: "qa",
|
|
6157
|
+
allowShellTool: true,
|
|
6158
|
+
sandboxMode: "host",
|
|
6159
|
+
}
|
|
6160
|
+
);
|
|
6161
|
+
const liveScopedTeeTestLogCommand = {
|
|
6162
|
+
toolName: "run_command",
|
|
6163
|
+
ok: true,
|
|
6164
|
+
exitCode: 0,
|
|
6165
|
+
args: {
|
|
6166
|
+
command:
|
|
6167
|
+
`cd ${JSON.stringify(workspace)} && ` +
|
|
6168
|
+
`TASK=${scopedRelativeTaskRoot} && LOG="$TASK/document-reader-test.log" && ` +
|
|
6169
|
+
`{ echo "=== focused test ==="; python3 -m unittest discover -s tests -p 'test_reader.py' -v 2>&1; RC=$?; echo "EXIT=$RC"; } ` +
|
|
6170
|
+
`| tee "$LOG"; grep -q '^OK$' "$LOG"; echo "DONE_EXIT=$?"`,
|
|
6171
|
+
},
|
|
6172
|
+
projectMutationPaths: [],
|
|
6173
|
+
stdout: "Ran 7 tests in 0.01s\nOK\nEXIT=0\nDONE_EXIT=0\n",
|
|
6174
|
+
stderr: "",
|
|
6175
|
+
};
|
|
6176
|
+
recordProjectVerificationOutcome(
|
|
6177
|
+
scopedTaskState,
|
|
6178
|
+
liveScopedTeeTestLogCommand,
|
|
6179
|
+
{
|
|
6180
|
+
commandCwd: workspace,
|
|
6181
|
+
taskProfile: "qa",
|
|
6182
|
+
allowShellTool: true,
|
|
6183
|
+
sandboxMode: "host",
|
|
6184
|
+
}
|
|
6185
|
+
);
|
|
6186
|
+
const unsafeScopedLogState = {
|
|
6187
|
+
goal: scopedTaskGoal,
|
|
6188
|
+
commandCwd: workspace,
|
|
6189
|
+
meta: {
|
|
6190
|
+
goalContract: {
|
|
6191
|
+
revision: 1,
|
|
6192
|
+
taskGoal: scopedTaskGoal,
|
|
6193
|
+
currentRequest: "Create and verify output/result.json.",
|
|
6194
|
+
},
|
|
6195
|
+
},
|
|
6196
|
+
};
|
|
6197
|
+
const outsideScopedGroupTestLogCommand = {
|
|
6198
|
+
toolName: "run_command",
|
|
6199
|
+
ok: true,
|
|
6200
|
+
exitCode: 0,
|
|
6201
|
+
args: {
|
|
6202
|
+
command:
|
|
6203
|
+
`cd ${JSON.stringify(workspace)} && LOG="source-tree-test.log" && ` +
|
|
6204
|
+
`{ python3 -m unittest discover -s tests -p 'test_reader.py' -v; RC=$?; echo "EXIT=$RC"; } ` +
|
|
6205
|
+
`> "$LOG" 2>&1; tail -n 6 "$LOG"`,
|
|
6206
|
+
},
|
|
6207
|
+
projectMutationPaths: [],
|
|
6208
|
+
stdout: "Ran 7 tests in 0.01s\nOK\nEXIT=0\n",
|
|
6209
|
+
stderr: "",
|
|
6210
|
+
};
|
|
6211
|
+
recordProjectVerificationOutcome(
|
|
6212
|
+
unsafeScopedLogState,
|
|
6213
|
+
outsideScopedGroupTestLogCommand,
|
|
6214
|
+
{
|
|
6215
|
+
commandCwd: workspace,
|
|
6216
|
+
taskProfile: "qa",
|
|
6217
|
+
allowShellTool: true,
|
|
6218
|
+
sandboxMode: "host",
|
|
6219
|
+
}
|
|
6220
|
+
);
|
|
6221
|
+
const sourceWriteAfterScopedLogState = {
|
|
6222
|
+
goal: scopedTaskGoal,
|
|
6223
|
+
commandCwd: workspace,
|
|
6224
|
+
meta: {
|
|
6225
|
+
goalContract: {
|
|
6226
|
+
revision: 1,
|
|
6227
|
+
taskGoal: scopedTaskGoal,
|
|
6228
|
+
currentRequest: "Create and verify output/result.json.",
|
|
6229
|
+
},
|
|
6230
|
+
},
|
|
6231
|
+
};
|
|
6232
|
+
const sourceWriteAfterScopedLogCommand = {
|
|
6233
|
+
toolName: "run_command",
|
|
6234
|
+
ok: true,
|
|
6235
|
+
exitCode: 0,
|
|
6236
|
+
args: {
|
|
6237
|
+
command:
|
|
6238
|
+
`cd ${JSON.stringify(workspace)} && ` +
|
|
6239
|
+
`TASK=${scopedRelativeTaskRoot} && LOG="$TASK/document-reader-test.log" && ` +
|
|
6240
|
+
`{ python3 -m unittest discover -s tests -p 'test_reader.py' -v; RC=$?; echo "EXIT=$RC"; } ` +
|
|
6241
|
+
`> "$LOG" 2>&1; touch src/agent-runner.js; tail -n 6 "$LOG"`,
|
|
6242
|
+
},
|
|
6243
|
+
projectMutationPaths: [],
|
|
6244
|
+
stdout: "Ran 7 tests in 0.01s\nOK\nEXIT=0\n",
|
|
6245
|
+
stderr: "",
|
|
6246
|
+
};
|
|
6247
|
+
recordProjectVerificationOutcome(
|
|
6248
|
+
sourceWriteAfterScopedLogState,
|
|
6249
|
+
sourceWriteAfterScopedLogCommand,
|
|
6250
|
+
{
|
|
6251
|
+
commandCwd: workspace,
|
|
6252
|
+
taskProfile: "qa",
|
|
6253
|
+
allowShellTool: true,
|
|
6254
|
+
sandboxMode: "host",
|
|
6255
|
+
}
|
|
6256
|
+
);
|
|
6257
|
+
const spoofedCapturedStatusState = {
|
|
6258
|
+
goal: scopedTaskGoal,
|
|
6259
|
+
commandCwd: workspace,
|
|
6260
|
+
meta: {
|
|
6261
|
+
goalContract: {
|
|
6262
|
+
revision: 1,
|
|
6263
|
+
taskGoal: scopedTaskGoal,
|
|
6264
|
+
currentRequest: "Create and verify output/result.json.",
|
|
6265
|
+
},
|
|
6266
|
+
},
|
|
6267
|
+
};
|
|
6268
|
+
const spoofedCapturedStatusCommand = {
|
|
6269
|
+
toolName: "run_command",
|
|
6270
|
+
ok: true,
|
|
6271
|
+
exitCode: 0,
|
|
6272
|
+
args: {
|
|
6273
|
+
command:
|
|
6274
|
+
`cd ${JSON.stringify(workspace)} && ` +
|
|
6275
|
+
`TASK=${scopedRelativeTaskRoot} && LOG="$TASK/document-reader-test.log" && ` +
|
|
6276
|
+
`{ python3 -m unittest discover -s tests -p 'test_reader.py' -v; RC=$?; echo "EXIT=$RC"; } ` +
|
|
6277
|
+
`> "$LOG" 2>&1; tail -n 6 "$LOG"; echo "EXIT=0"`,
|
|
6278
|
+
},
|
|
6279
|
+
projectMutationPaths: [],
|
|
6280
|
+
stdout: "FAILED (failures=1)\nEXIT=1\nEXIT=0\n",
|
|
6281
|
+
stderr: "",
|
|
6282
|
+
};
|
|
6283
|
+
recordProjectVerificationOutcome(
|
|
6284
|
+
spoofedCapturedStatusState,
|
|
6285
|
+
spoofedCapturedStatusCommand,
|
|
6286
|
+
{
|
|
6287
|
+
commandCwd: workspace,
|
|
6288
|
+
taskProfile: "qa",
|
|
6289
|
+
allowShellTool: true,
|
|
6290
|
+
sandboxMode: "host",
|
|
6291
|
+
}
|
|
6292
|
+
);
|
|
6061
6293
|
const nullRedirectValidationCommand = {
|
|
6062
6294
|
toolName: "run_command",
|
|
6063
6295
|
ok: true,
|
|
@@ -6095,6 +6327,17 @@ try {
|
|
|
6095
6327
|
scopedManifestCommand.scopedTaskArtifactWrite === true &&
|
|
6096
6328
|
pythonScopedManifestCommand.scopedTaskArtifactWrite === true &&
|
|
6097
6329
|
scopedCompoundTestLogCommand.scopedTaskArtifactWrite === true &&
|
|
6330
|
+
liveScopedGroupTestLogCommand.scopedTaskArtifactWrite === true &&
|
|
6331
|
+
liveScopedGroupTestLogCommand.projectTest?.passed === true &&
|
|
6332
|
+
liveScopedGroupTestLogCommand.projectTest?.explicitExitStatus === 0 &&
|
|
6333
|
+
liveScopedTeeTestLogCommand.scopedTaskArtifactWrite === true &&
|
|
6334
|
+
liveScopedTeeTestLogCommand.projectTest?.passed === true &&
|
|
6335
|
+
liveScopedTeeTestLogCommand.projectTest?.explicitExitStatus === 0 &&
|
|
6336
|
+
unsafeScopedLogState.meta.projectVerification?.mutationRevision === 1 &&
|
|
6337
|
+
outsideScopedGroupTestLogCommand.scopedTaskArtifactWrite !== true &&
|
|
6338
|
+
sourceWriteAfterScopedLogState.meta.projectVerification?.mutationRevision === 1 &&
|
|
6339
|
+
sourceWriteAfterScopedLogCommand.scopedTaskArtifactWrite !== true &&
|
|
6340
|
+
spoofedCapturedStatusCommand.projectTest === undefined &&
|
|
6098
6341
|
nullRedirectValidationCommand.readOnlyArtifactValidation === true,
|
|
6099
6342
|
`task-scoped delivery bookkeeping invalidated requested artifact evidence: ${JSON.stringify({
|
|
6100
6343
|
requestedOutputRevision,
|
|
@@ -6107,6 +6350,29 @@ try {
|
|
|
6107
6350
|
pythonScopedManifestCommand.scopedTaskArtifactWrite,
|
|
6108
6351
|
scopedCompoundTestLogCommand:
|
|
6109
6352
|
scopedCompoundTestLogCommand.scopedTaskArtifactWrite,
|
|
6353
|
+
liveScopedGroupTestLogCommand: {
|
|
6354
|
+
scopedTaskArtifactWrite: liveScopedGroupTestLogCommand.scopedTaskArtifactWrite,
|
|
6355
|
+
projectTest: liveScopedGroupTestLogCommand.projectTest,
|
|
6356
|
+
},
|
|
6357
|
+
liveScopedTeeTestLogCommand: {
|
|
6358
|
+
scopedTaskArtifactWrite: liveScopedTeeTestLogCommand.scopedTaskArtifactWrite,
|
|
6359
|
+
projectTest: liveScopedTeeTestLogCommand.projectTest,
|
|
6360
|
+
},
|
|
6361
|
+
outsideScopedGroupTestLogCommand: {
|
|
6362
|
+
mutationRevision:
|
|
6363
|
+
unsafeScopedLogState.meta.projectVerification?.mutationRevision,
|
|
6364
|
+
scopedTaskArtifactWrite:
|
|
6365
|
+
outsideScopedGroupTestLogCommand.scopedTaskArtifactWrite,
|
|
6366
|
+
},
|
|
6367
|
+
sourceWriteAfterScopedLogCommand: {
|
|
6368
|
+
mutationRevision:
|
|
6369
|
+
sourceWriteAfterScopedLogState.meta.projectVerification?.mutationRevision,
|
|
6370
|
+
scopedTaskArtifactWrite:
|
|
6371
|
+
sourceWriteAfterScopedLogCommand.scopedTaskArtifactWrite,
|
|
6372
|
+
},
|
|
6373
|
+
spoofedCapturedStatusCommand: {
|
|
6374
|
+
projectTest: spoofedCapturedStatusCommand.projectTest,
|
|
6375
|
+
},
|
|
6110
6376
|
nullRedirectValidationCommand:
|
|
6111
6377
|
nullRedirectValidationCommand.readOnlyArtifactValidation,
|
|
6112
6378
|
})}`
|
package/src/agent-runner.js
CHANGED
|
@@ -4967,6 +4967,130 @@ function normalizeProjectCommand(command = "") {
|
|
|
4967
4967
|
return canonicalizeShellCommand(command);
|
|
4968
4968
|
}
|
|
4969
4969
|
|
|
4970
|
+
function shellBraceGroups(value = "") {
|
|
4971
|
+
const text = String(value || "");
|
|
4972
|
+
const groups = [];
|
|
4973
|
+
let quote = "";
|
|
4974
|
+
let escaped = false;
|
|
4975
|
+
let start = -1;
|
|
4976
|
+
let depth = 0;
|
|
4977
|
+
|
|
4978
|
+
const isGroupOpen = (index) => {
|
|
4979
|
+
if (text[index] !== "{" || text[index - 1] === "$") return false;
|
|
4980
|
+
const before = text[index - 1] || "";
|
|
4981
|
+
const after = text[index + 1] || "";
|
|
4982
|
+
return (!before || /[\s;&|()]/u.test(before)) && /\s/u.test(after);
|
|
4983
|
+
};
|
|
4984
|
+
const isGroupClose = (index) => {
|
|
4985
|
+
if (text[index] !== "}") return false;
|
|
4986
|
+
const before = text[index - 1] || "";
|
|
4987
|
+
const after = text[index + 1] || "";
|
|
4988
|
+
return /\s/u.test(before) && (!after || /[\s;&|<>)]/u.test(after));
|
|
4989
|
+
};
|
|
4990
|
+
|
|
4991
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
4992
|
+
const char = text[index];
|
|
4993
|
+
if (quote === "'") {
|
|
4994
|
+
if (char === "'") quote = "";
|
|
4995
|
+
continue;
|
|
4996
|
+
}
|
|
4997
|
+
if (escaped) {
|
|
4998
|
+
escaped = false;
|
|
4999
|
+
continue;
|
|
5000
|
+
}
|
|
5001
|
+
if (char === "\\") {
|
|
5002
|
+
escaped = true;
|
|
5003
|
+
continue;
|
|
5004
|
+
}
|
|
5005
|
+
if (quote === '"') {
|
|
5006
|
+
if (char === '"') quote = "";
|
|
5007
|
+
continue;
|
|
5008
|
+
}
|
|
5009
|
+
if (char === "'" || char === '"') {
|
|
5010
|
+
quote = char;
|
|
5011
|
+
continue;
|
|
5012
|
+
}
|
|
5013
|
+
if (start < 0 && isGroupOpen(index)) {
|
|
5014
|
+
start = index;
|
|
5015
|
+
depth = 1;
|
|
5016
|
+
continue;
|
|
5017
|
+
}
|
|
5018
|
+
if (start >= 0 && isGroupOpen(index)) {
|
|
5019
|
+
depth += 1;
|
|
5020
|
+
continue;
|
|
5021
|
+
}
|
|
5022
|
+
if (start >= 0 && isGroupClose(index)) {
|
|
5023
|
+
depth -= 1;
|
|
5024
|
+
if (depth > 0) continue;
|
|
5025
|
+
groups.push({
|
|
5026
|
+
start,
|
|
5027
|
+
end: index,
|
|
5028
|
+
body: text.slice(start + 1, index).trim(),
|
|
5029
|
+
});
|
|
5030
|
+
start = -1;
|
|
5031
|
+
depth = 0;
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
return groups;
|
|
5035
|
+
}
|
|
5036
|
+
|
|
5037
|
+
function parseCapturedTestExitStatusWrapper(value = "") {
|
|
5038
|
+
const normalized = normalizeProjectCommand(value);
|
|
5039
|
+
if (!normalized) return null;
|
|
5040
|
+
const label = "(?:EXIT|STATUS|RESULT)(?:_CODE)?";
|
|
5041
|
+
for (const group of shellBraceGroups(normalized)) {
|
|
5042
|
+
const sequence = parseTopLevelShellSequence(group.body);
|
|
5043
|
+
if (
|
|
5044
|
+
sequence.openQuote ||
|
|
5045
|
+
sequence.trailingEscape ||
|
|
5046
|
+
(sequence.trailingSeparator && sequence.trailingSeparator !== ";") ||
|
|
5047
|
+
sequence.commands.length < 3
|
|
5048
|
+
) {
|
|
5049
|
+
continue;
|
|
5050
|
+
}
|
|
5051
|
+
for (let index = 1; index < sequence.commands.length - 1; index += 1) {
|
|
5052
|
+
const statusAssignment = String(sequence.commands[index] || "")
|
|
5053
|
+
.trim()
|
|
5054
|
+
.match(/^([A-Za-z_]\w*)\s*=\s*\$\?$/u);
|
|
5055
|
+
if (!statusAssignment) continue;
|
|
5056
|
+
const statusVariable = statusAssignment[1];
|
|
5057
|
+
const escapedVariable = statusVariable.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
5058
|
+
const statusEcho = String(sequence.commands[index + 1] || "").trim();
|
|
5059
|
+
const echoPattern = new RegExp(
|
|
5060
|
+
`^echo\\s+(?:-n\\s+)?["']?(?<label>${label})\\s*[:=]\\s*\\$(?:\\{${escapedVariable}\\}|${escapedVariable})["']?$`,
|
|
5061
|
+
"iu"
|
|
5062
|
+
);
|
|
5063
|
+
const echoMatch = statusEcho.match(echoPattern);
|
|
5064
|
+
if (!echoMatch) continue;
|
|
5065
|
+
const outsideGroup = `${normalized.slice(0, group.start)} ${normalized.slice(group.end + 1)}`;
|
|
5066
|
+
if (
|
|
5067
|
+
/\b(?:echo|printf)\b[^;&|\n]{0,240}\b(?:EXIT|STATUS|RESULT)(?:_CODE)?\s*[:=]/iu.test(
|
|
5068
|
+
outsideGroup
|
|
5069
|
+
)
|
|
5070
|
+
) {
|
|
5071
|
+
continue;
|
|
5072
|
+
}
|
|
5073
|
+
const testCommand = normalizeProjectCommand(sequence.commands[index - 1]);
|
|
5074
|
+
if (classifyCommand(testCommand).substantiveTest !== true) continue;
|
|
5075
|
+
return {
|
|
5076
|
+
command: testCommand,
|
|
5077
|
+
wrappedCommand: normalized,
|
|
5078
|
+
label: String(echoMatch.groups?.label || "").toUpperCase(),
|
|
5079
|
+
statusSource: "captured-group",
|
|
5080
|
+
statusVariable,
|
|
5081
|
+
};
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
return null;
|
|
5085
|
+
}
|
|
5086
|
+
|
|
5087
|
+
function projectExitStatusWrapper(command = "") {
|
|
5088
|
+
return (
|
|
5089
|
+
parseNonMutatingExitStatusWrapper(command) ||
|
|
5090
|
+
parseCapturedTestExitStatusWrapper(command)
|
|
5091
|
+
);
|
|
5092
|
+
}
|
|
5093
|
+
|
|
4970
5094
|
function normalizeLeadingWorkspaceCd(command = "", config = {}) {
|
|
4971
5095
|
const normalized = normalizeProjectCommand(command);
|
|
4972
5096
|
const sequence = parseTopLevelShellSequence(normalized);
|
|
@@ -5037,7 +5161,7 @@ function isRetainedExactVerificationCommand(command = "", config = {}) {
|
|
|
5037
5161
|
export function isSubstantiveTestCommand(command = "", config = {}) {
|
|
5038
5162
|
if (isRetainedExactVerificationCommand(command, config)) return true;
|
|
5039
5163
|
const normalized = normalizeProjectCommand(normalizeCommandForPolicy(command, config));
|
|
5040
|
-
const text =
|
|
5164
|
+
const text = projectExitStatusWrapper(normalized)?.command || normalized;
|
|
5041
5165
|
if (!text) return false;
|
|
5042
5166
|
const classification = classifyCommand(text);
|
|
5043
5167
|
if (classification.substantiveTest !== true) return false;
|
|
@@ -5142,13 +5266,31 @@ function testRunRepresentsInvalidInvocation(testRun = {}) {
|
|
|
5142
5266
|
|
|
5143
5267
|
function explicitExitProbeStatus(command = "", result = {}) {
|
|
5144
5268
|
const normalizedCommand = normalizeProjectCommand(command);
|
|
5145
|
-
const exitProbe =
|
|
5146
|
-
if (!exitProbe)
|
|
5269
|
+
const exitProbe = projectExitStatusWrapper(normalizedCommand);
|
|
5270
|
+
if (!exitProbe) {
|
|
5271
|
+
return {
|
|
5272
|
+
present: false,
|
|
5273
|
+
status: null,
|
|
5274
|
+
command: normalizedCommand,
|
|
5275
|
+
wrappedCommand: normalizedCommand,
|
|
5276
|
+
};
|
|
5277
|
+
}
|
|
5278
|
+
|
|
5279
|
+
let status = parseExplicitExitStatus(result.stdout || "");
|
|
5280
|
+
if (status === null && exitProbe.statusSource === "captured-group") {
|
|
5281
|
+
const matches = [
|
|
5282
|
+
...String(result.stdout || "").matchAll(
|
|
5283
|
+
/(?:^|\n)\s*(?:EXIT|STATUS|RESULT)(?:_CODE)?\s*[:=]\s*(-?\d+)\s*(?=\n|$)/giu
|
|
5284
|
+
),
|
|
5285
|
+
];
|
|
5286
|
+
if (matches.length) status = Number(matches.at(-1)[1]);
|
|
5287
|
+
}
|
|
5147
5288
|
|
|
5148
5289
|
return {
|
|
5149
5290
|
present: true,
|
|
5150
|
-
status
|
|
5291
|
+
status,
|
|
5151
5292
|
command: exitProbe.command,
|
|
5293
|
+
wrappedCommand: exitProbe.wrappedCommand || exitProbe.command,
|
|
5152
5294
|
};
|
|
5153
5295
|
}
|
|
5154
5296
|
|
|
@@ -6717,7 +6859,10 @@ export function recordProjectVerificationOutcome(state = {}, toolResult = {}, co
|
|
|
6717
6859
|
);
|
|
6718
6860
|
const exitProbe = explicitExitProbeStatus(command, toolResult);
|
|
6719
6861
|
const mutationCommand = normalizeProjectCommand(
|
|
6720
|
-
normalizeCommandForPolicy(
|
|
6862
|
+
normalizeCommandForPolicy(
|
|
6863
|
+
exitProbe.wrappedCommand || exitProbe.command || command,
|
|
6864
|
+
config
|
|
6865
|
+
)
|
|
6721
6866
|
);
|
|
6722
6867
|
// The result proves the command already executed. Classify its semantic
|
|
6723
6868
|
// mutation capability independently of whether the current policy would
|
|
@@ -7385,7 +7530,7 @@ export function recordAlreadyCommittedRepositoryRepair(state = {}, toolResult =
|
|
|
7385
7530
|
|
|
7386
7531
|
function projectTestCommandKey(command = "") {
|
|
7387
7532
|
const normalized = normalizeProjectCommand(command);
|
|
7388
|
-
const exitProbe =
|
|
7533
|
+
const exitProbe = projectExitStatusWrapper(normalized);
|
|
7389
7534
|
const invocation = normalizeProjectCommand(exitProbe?.command || normalized)
|
|
7390
7535
|
// These wrappers alter bytecode/output handling, not the tests selected or
|
|
7391
7536
|
// executed. Treating them as distinct suites can leave an old failure
|
|
@@ -12766,40 +12911,106 @@ function commandWritesOnlyEvidenceMatching(command = "", pathMatches = () => fal
|
|
|
12766
12911
|
for (const match of normalized.matchAll(
|
|
12767
12912
|
/(?:^|[\s;&|])([A-Za-z_]\w*)\s*=\s*("[^"\n]+"|'[^'\n]+'|[^\s;&|]+)/g
|
|
12768
12913
|
)) {
|
|
12769
|
-
assignments.set(
|
|
12770
|
-
match[1],
|
|
12771
|
-
String(match[2] || "").replace(/^["']|["']$/g, "")
|
|
12772
|
-
);
|
|
12914
|
+
assignments.set(match[1], String(match[2] || "").replace(/^["']|["']$/g, ""));
|
|
12773
12915
|
}
|
|
12774
|
-
const resolveTarget = (value = "") => {
|
|
12775
|
-
|
|
12916
|
+
const resolveTarget = (value = "", seen = new Set()) => {
|
|
12917
|
+
let target = String(value || "")
|
|
12776
12918
|
.trim()
|
|
12777
12919
|
.replace(/^["']|["']$/g, "");
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12920
|
+
if (!target || /\$\(|`/u.test(target)) return "";
|
|
12921
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
12922
|
+
let unresolved = false;
|
|
12923
|
+
let changed = false;
|
|
12924
|
+
const expanded = target.replace(
|
|
12925
|
+
/\$(?:\{([A-Za-z_]\w*)\}|([A-Za-z_]\w*))/gu,
|
|
12926
|
+
(match, bracedName, bareName) => {
|
|
12927
|
+
const name = bracedName || bareName;
|
|
12928
|
+
if (!assignments.has(name) || seen.has(name)) {
|
|
12929
|
+
unresolved = true;
|
|
12930
|
+
return match;
|
|
12931
|
+
}
|
|
12932
|
+
const nextSeen = new Set(seen);
|
|
12933
|
+
nextSeen.add(name);
|
|
12934
|
+
const resolved = resolveTarget(assignments.get(name), nextSeen);
|
|
12935
|
+
if (!resolved) {
|
|
12936
|
+
unresolved = true;
|
|
12937
|
+
return match;
|
|
12938
|
+
}
|
|
12939
|
+
changed = true;
|
|
12940
|
+
return resolved;
|
|
12941
|
+
}
|
|
12942
|
+
);
|
|
12943
|
+
target = expanded;
|
|
12944
|
+
if (unresolved) return "";
|
|
12945
|
+
if (!changed) return target;
|
|
12946
|
+
}
|
|
12947
|
+
return "";
|
|
12782
12948
|
};
|
|
12783
|
-
const tokens =
|
|
12784
|
-
|
|
12949
|
+
const evidenceTargetsFromTee = (tokens = []) => {
|
|
12950
|
+
if (tokens[0] !== "tee") return [];
|
|
12785
12951
|
let index = 1;
|
|
12786
12952
|
if (["-a", "--append"].includes(tokens[index])) index += 1;
|
|
12787
12953
|
if (tokens[index] === "--") index += 1;
|
|
12788
|
-
|
|
12954
|
+
return tokens.slice(index).map((target) => resolveTarget(target));
|
|
12955
|
+
};
|
|
12956
|
+
const evidenceTargetsFromMkdir = (tokens = []) =>
|
|
12957
|
+
tokens[0] === "mkdir"
|
|
12958
|
+
? tokens
|
|
12959
|
+
.slice(1)
|
|
12960
|
+
.filter((token) => token !== "-p" && token !== "--")
|
|
12961
|
+
.map((target) => resolveTarget(target))
|
|
12962
|
+
: [];
|
|
12963
|
+
const boundedEchoProbeIsReadOnly = (segment = "") => {
|
|
12964
|
+
const source = String(segment || "").trim();
|
|
12965
|
+
const sourceTokens = tokenizeShellWords(source);
|
|
12966
|
+
if (!["echo", "printf"].includes(String(sourceTokens[0] || ""))) return false;
|
|
12967
|
+
if (source.includes("`") || !source.includes("$(")) return !source.includes("`");
|
|
12968
|
+
const substitutions = [];
|
|
12969
|
+
const remainder = source.replace(/\$\(([^()]*)\)/gu, (match, body) => {
|
|
12970
|
+
substitutions.push(String(body || ""));
|
|
12971
|
+
return "";
|
|
12972
|
+
});
|
|
12973
|
+
if (!substitutions.length || remainder.includes("$(")) return false;
|
|
12974
|
+
return substitutions.every((body) => {
|
|
12975
|
+
const sequence = parseTopLevelShellSequence(body);
|
|
12976
|
+
if (
|
|
12977
|
+
sequence.openQuote ||
|
|
12978
|
+
sequence.trailingEscape ||
|
|
12979
|
+
sequence.trailingSeparator ||
|
|
12980
|
+
!sequence.commands.length
|
|
12981
|
+
) {
|
|
12982
|
+
return false;
|
|
12983
|
+
}
|
|
12984
|
+
return sequence.commands.every((candidate) => {
|
|
12985
|
+
const candidateTokens = tokenizeShellWords(candidate);
|
|
12986
|
+
if (["echo", "printf"].includes(String(candidateTokens[0] || ""))) {
|
|
12987
|
+
return !String(candidate || "").includes("$(") && !String(candidate || "").includes("`");
|
|
12988
|
+
}
|
|
12989
|
+
if (["[", "test"].includes(String(candidateTokens[0] || ""))) {
|
|
12990
|
+
const operands = candidateTokens.filter(
|
|
12991
|
+
(token) => !["[", "]", "test", "!", "-e", "-f", "-r", "-s"].includes(token)
|
|
12992
|
+
);
|
|
12993
|
+
return operands.length > 0 && operands.every((operand) => Boolean(resolveTarget(operand)));
|
|
12994
|
+
}
|
|
12995
|
+
const policy = classifyCommand(candidate);
|
|
12996
|
+
return policy.writesWorkspace !== true && policy.mayMutateProject !== true;
|
|
12997
|
+
});
|
|
12998
|
+
});
|
|
12999
|
+
};
|
|
13000
|
+
const tokens = tokenizeShellWords(normalized);
|
|
13001
|
+
if (tokens[0] === "tee") {
|
|
13002
|
+
const targets = evidenceTargetsFromTee(tokens);
|
|
12789
13003
|
return targets.length > 0 && targets.every(pathMatches);
|
|
12790
13004
|
}
|
|
12791
13005
|
if (tokens[0] === "mkdir") {
|
|
12792
|
-
const targets = tokens
|
|
12793
|
-
.slice(1)
|
|
12794
|
-
.filter((token) => token !== "-p" && token !== "--")
|
|
12795
|
-
.map(resolveTarget);
|
|
13006
|
+
const targets = evidenceTargetsFromMkdir(tokens);
|
|
12796
13007
|
return targets.length > 0 && targets.every(pathMatches);
|
|
12797
13008
|
}
|
|
12798
13009
|
|
|
12799
13010
|
let strippedEvidenceRedirect = false;
|
|
12800
13011
|
let rejectedRedirect = false;
|
|
12801
13012
|
const withoutEvidenceRedirects = normalized.replace(
|
|
12802
|
-
/(^|\s)(?:\d*>>?|&>>?)\s*("[^"]+"|'[^']+'
|
|
13013
|
+
/(^|\s)(?:\d*>>?|&>>?)\s*("[^"]+"|'[^']+'|&\d+|[^\s;&|]+)/g,
|
|
12803
13014
|
(match, prefix, target) => {
|
|
12804
13015
|
const resolvedTarget = resolveTarget(target);
|
|
12805
13016
|
if (/^(?:&\d+|\/dev\/null)$/u.test(resolvedTarget)) return prefix;
|
|
@@ -12811,15 +13022,19 @@ function commandWritesOnlyEvidenceMatching(command = "", pathMatches = () => fal
|
|
|
12811
13022
|
return prefix;
|
|
12812
13023
|
}
|
|
12813
13024
|
).trim();
|
|
12814
|
-
if (rejectedRedirect || !
|
|
13025
|
+
if (rejectedRedirect || !withoutEvidenceRedirects) {
|
|
12815
13026
|
return false;
|
|
12816
13027
|
}
|
|
12817
13028
|
const sequence = parseTopLevelShellSequence(withoutEvidenceRedirects);
|
|
12818
13029
|
if (sequence.openQuote || sequence.trailingEscape || sequence.trailingSeparator) {
|
|
12819
13030
|
return false;
|
|
12820
13031
|
}
|
|
12821
|
-
|
|
13032
|
+
let sawEvidenceWrite = strippedEvidenceRedirect;
|
|
13033
|
+
const safeSequence = sequence.commands.every((segment) => {
|
|
12822
13034
|
const withoutAssignments = String(segment || "")
|
|
13035
|
+
.trim()
|
|
13036
|
+
.replace(/^\{\s*/u, "")
|
|
13037
|
+
.replace(/\s*\}$/u, "")
|
|
12823
13038
|
.trim()
|
|
12824
13039
|
.replace(
|
|
12825
13040
|
/^(?:[A-Za-z_]\w*=(?:"[^"\n]*"|'[^'\n]*'|[^\s;&|]+)\s*)+/u,
|
|
@@ -12828,6 +13043,19 @@ function commandWritesOnlyEvidenceMatching(command = "", pathMatches = () => fal
|
|
|
12828
13043
|
.trim();
|
|
12829
13044
|
if (!withoutAssignments) return true;
|
|
12830
13045
|
const commandTokens = tokenizeShellWords(withoutAssignments);
|
|
13046
|
+
const teeTargets = evidenceTargetsFromTee(commandTokens);
|
|
13047
|
+
if (teeTargets.length) {
|
|
13048
|
+
const safe = teeTargets.every(pathMatches);
|
|
13049
|
+
if (safe) sawEvidenceWrite = true;
|
|
13050
|
+
return safe;
|
|
13051
|
+
}
|
|
13052
|
+
const mkdirTargets = evidenceTargetsFromMkdir(commandTokens);
|
|
13053
|
+
if (mkdirTargets.length) {
|
|
13054
|
+
const safe = mkdirTargets.every(pathMatches);
|
|
13055
|
+
if (safe) sawEvidenceWrite = true;
|
|
13056
|
+
return safe;
|
|
13057
|
+
}
|
|
13058
|
+
if (boundedEchoProbeIsReadOnly(withoutAssignments)) return true;
|
|
12831
13059
|
if (
|
|
12832
13060
|
["cat", "cd", "head", "ls", "stat", "tail", "wc"].includes(
|
|
12833
13061
|
String(commandTokens[0] || "")
|
|
@@ -12841,6 +13069,7 @@ function commandWritesOnlyEvidenceMatching(command = "", pathMatches = () => fal
|
|
|
12841
13069
|
underlyingPolicy.mayMutateProject !== true
|
|
12842
13070
|
);
|
|
12843
13071
|
});
|
|
13072
|
+
return safeSequence && sawEvidenceWrite;
|
|
12844
13073
|
}
|
|
12845
13074
|
|
|
12846
13075
|
function commandWritesOnlyPrivateVerificationEvidence(command = "") {
|
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*\\$\\?)$`,
|