@lazyingart/agintiflow 0.20.323 → 0.20.325
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-dynamic-step-budget.js +125 -0
- package/src/agent-runner.js +158 -5
- package/src/command-policy.js +48 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.325",
|
|
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",
|
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
recordAlreadyCommittedRepositoryRepair,
|
|
81
81
|
recordDurableEvidenceCategories,
|
|
82
82
|
recordProjectVerificationOutcome,
|
|
83
|
+
recordFailedCommandAttempt,
|
|
83
84
|
repositoryStateInspectionCommand,
|
|
84
85
|
recordExactOutputProgress,
|
|
85
86
|
recordStaticDiscoveryProgress,
|
|
@@ -94,6 +95,7 @@ import {
|
|
|
94
95
|
forbiddenCurrentTestRerunBlock,
|
|
95
96
|
unchangedFailedTestRerunBlock,
|
|
96
97
|
repeatedNoProgressToolBlock,
|
|
98
|
+
runCommandResultHasDurableProgress,
|
|
97
99
|
redundantCadValidationAliasPatchBlock,
|
|
98
100
|
regressiveInversePatchBlock,
|
|
99
101
|
repeatedSuccessfulMutationBlock,
|
|
@@ -11030,6 +11032,62 @@ try {
|
|
|
11030
11032
|
})?.category === "repeated-no-progress-call",
|
|
11031
11033
|
"a third identical unchanged shell probe was not blocked"
|
|
11032
11034
|
);
|
|
11035
|
+
const missingCanvasPath = path.join(workspace, "report.pdf");
|
|
11036
|
+
const repeatedCanvasState = {
|
|
11037
|
+
meta: {
|
|
11038
|
+
toolLoop: {
|
|
11039
|
+
stagnationEpoch: 4,
|
|
11040
|
+
recent: [
|
|
11041
|
+
{
|
|
11042
|
+
toolName: "send_to_canvas",
|
|
11043
|
+
ok: false,
|
|
11044
|
+
blocked: true,
|
|
11045
|
+
stagnationEpoch: 4,
|
|
11046
|
+
error: `Canvas path does not exist or is not a file: ${missingCanvasPath}`,
|
|
11047
|
+
},
|
|
11048
|
+
{
|
|
11049
|
+
toolName: "send_to_canvas",
|
|
11050
|
+
ok: false,
|
|
11051
|
+
blocked: true,
|
|
11052
|
+
stagnationEpoch: 4,
|
|
11053
|
+
error: `Canvas path does not exist or is not a file: ${missingCanvasPath}`,
|
|
11054
|
+
},
|
|
11055
|
+
],
|
|
11056
|
+
},
|
|
11057
|
+
},
|
|
11058
|
+
};
|
|
11059
|
+
assert(
|
|
11060
|
+
repeatedNoProgressToolBlock(
|
|
11061
|
+
repeatedCanvasState,
|
|
11062
|
+
"send_to_canvas",
|
|
11063
|
+
{
|
|
11064
|
+
path: missingCanvasPath,
|
|
11065
|
+
title: "Changed title",
|
|
11066
|
+
note: "A cosmetically different note",
|
|
11067
|
+
},
|
|
11068
|
+
{ commandCwd: workspace }
|
|
11069
|
+
)?.category === "repeated-no-progress-call",
|
|
11070
|
+
"changed canvas metadata allowed the same missing artifact target to loop"
|
|
11071
|
+
);
|
|
11072
|
+
assert(
|
|
11073
|
+
repeatedNoProgressToolBlock(
|
|
11074
|
+
repeatedCanvasState,
|
|
11075
|
+
"send_to_canvas",
|
|
11076
|
+
{ path: path.join(workspace, "different.pdf"), title: "Different artifact" },
|
|
11077
|
+
{ commandCwd: workspace }
|
|
11078
|
+
) === null,
|
|
11079
|
+
"a different canvas artifact was blocked by unrelated missing-path history"
|
|
11080
|
+
);
|
|
11081
|
+
await fs.writeFile(missingCanvasPath, "%PDF-1.4\n", "utf8");
|
|
11082
|
+
assert(
|
|
11083
|
+
repeatedNoProgressToolBlock(
|
|
11084
|
+
repeatedCanvasState,
|
|
11085
|
+
"send_to_canvas",
|
|
11086
|
+
{ path: missingCanvasPath, title: "Now available" },
|
|
11087
|
+
{ commandCwd: workspace }
|
|
11088
|
+
) === null,
|
|
11089
|
+
"a canvas artifact created after prior failures remained blocked"
|
|
11090
|
+
);
|
|
11033
11091
|
assert(
|
|
11034
11092
|
repeatedNoProgressToolBlock(repeatedProbeState, "run_command", repeatedProbeArgs, {
|
|
11035
11093
|
commandCwd: workspace,
|
|
@@ -11103,6 +11161,73 @@ try {
|
|
|
11103
11161
|
)?.category === "repeated-no-progress-call",
|
|
11104
11162
|
"dynamic failure output allowed an unchanged failing command to loop"
|
|
11105
11163
|
);
|
|
11164
|
+
assertStrict.equal(
|
|
11165
|
+
runCommandResultHasDurableProgress({
|
|
11166
|
+
toolName: "run_command",
|
|
11167
|
+
ok: true,
|
|
11168
|
+
commandPolicy: classifyCommand(
|
|
11169
|
+
'find /aginti-env -type f -name "pdflatex" | xargs ls -la | grep -v "not found"'
|
|
11170
|
+
),
|
|
11171
|
+
}),
|
|
11172
|
+
false,
|
|
11173
|
+
"a bounded find/xargs/grep probe fabricated durable workspace progress"
|
|
11174
|
+
);
|
|
11175
|
+
assertStrict.equal(
|
|
11176
|
+
runCommandResultHasDurableProgress({
|
|
11177
|
+
toolName: "run_command",
|
|
11178
|
+
ok: true,
|
|
11179
|
+
commandPolicy: {
|
|
11180
|
+
category: "general-shell",
|
|
11181
|
+
writesWorkspace: true,
|
|
11182
|
+
},
|
|
11183
|
+
projectMutationPaths: ["src/example.js"],
|
|
11184
|
+
}),
|
|
11185
|
+
true,
|
|
11186
|
+
"an observed shell mutation did not count as durable workspace progress"
|
|
11187
|
+
);
|
|
11188
|
+
const compactedFailureLoop = {
|
|
11189
|
+
stagnationEpoch: 4,
|
|
11190
|
+
recent: [],
|
|
11191
|
+
};
|
|
11192
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
11193
|
+
recordFailedCommandAttempt(compactedFailureLoop, {
|
|
11194
|
+
signature: repeatedProbeState.meta.toolLoop.recent[0].signature,
|
|
11195
|
+
toolName: "run_command",
|
|
11196
|
+
ok: false,
|
|
11197
|
+
blocked: false,
|
|
11198
|
+
noProgressProbe: true,
|
|
11199
|
+
outcomeFingerprint: `failure-${attempt}`,
|
|
11200
|
+
stagnationEpoch: 4,
|
|
11201
|
+
goalRevision: 1,
|
|
11202
|
+
mutationRevision: 0,
|
|
11203
|
+
});
|
|
11204
|
+
}
|
|
11205
|
+
const compactedRepeatedFailureState = {
|
|
11206
|
+
meta: {
|
|
11207
|
+
toolLoop: compactedFailureLoop,
|
|
11208
|
+
},
|
|
11209
|
+
};
|
|
11210
|
+
assert(
|
|
11211
|
+
repeatedNoProgressToolBlock(
|
|
11212
|
+
compactedRepeatedFailureState,
|
|
11213
|
+
"run_command",
|
|
11214
|
+
repeatedProbeArgs,
|
|
11215
|
+
{ commandCwd: workspace }
|
|
11216
|
+
)?.category === "repeated-no-progress-call",
|
|
11217
|
+
"bounded failed-command history was lost after recent tool history rolled off"
|
|
11218
|
+
);
|
|
11219
|
+
const compactedFailureAfterMutation = structuredClone(compactedRepeatedFailureState);
|
|
11220
|
+
compactedFailureAfterMutation.meta.toolLoop.stagnationEpoch = 5;
|
|
11221
|
+
assertStrict.equal(
|
|
11222
|
+
repeatedNoProgressToolBlock(
|
|
11223
|
+
compactedFailureAfterMutation,
|
|
11224
|
+
"run_command",
|
|
11225
|
+
repeatedProbeArgs,
|
|
11226
|
+
{ commandCwd: workspace }
|
|
11227
|
+
),
|
|
11228
|
+
null,
|
|
11229
|
+
"a verified later state change did not release retained failed-command history"
|
|
11230
|
+
);
|
|
11106
11231
|
const newlyAuthoritativeVerificationState = structuredClone(repeatedFailureState);
|
|
11107
11232
|
newlyAuthoritativeVerificationState.meta.projectVerification = {
|
|
11108
11233
|
mutationRevision: 6,
|
package/src/agent-runner.js
CHANGED
|
@@ -10956,11 +10956,12 @@ function expectedRepeatedObservationCommand(command = "") {
|
|
|
10956
10956
|
);
|
|
10957
10957
|
}
|
|
10958
10958
|
|
|
10959
|
-
function runCommandResultHasDurableProgress(toolResult = {}) {
|
|
10959
|
+
export function runCommandResultHasDurableProgress(toolResult = {}) {
|
|
10960
10960
|
const policy = toolResult.commandPolicy || {};
|
|
10961
10961
|
const policyAllowsMutation =
|
|
10962
|
-
policy.
|
|
10963
|
-
(policy.mayMutateProject ===
|
|
10962
|
+
policy.semanticMayMutateProject !== false &&
|
|
10963
|
+
(policy.mayMutateProject === true ||
|
|
10964
|
+
(policy.mayMutateProject === undefined && policy.writesWorkspace === true));
|
|
10964
10965
|
return Boolean(
|
|
10965
10966
|
policyAllowsMutation ||
|
|
10966
10967
|
policy.substantiveTest === true ||
|
|
@@ -10971,6 +10972,54 @@ function runCommandResultHasDurableProgress(toolResult = {}) {
|
|
|
10971
10972
|
);
|
|
10972
10973
|
}
|
|
10973
10974
|
|
|
10975
|
+
function failedCommandAttempt(toolLoop = {}, signature = "", stagnationEpoch = 0) {
|
|
10976
|
+
return (Array.isArray(toolLoop.failedCommandAttempts)
|
|
10977
|
+
? toolLoop.failedCommandAttempts
|
|
10978
|
+
: []
|
|
10979
|
+
).find(
|
|
10980
|
+
(entry) =>
|
|
10981
|
+
entry?.signature === signature &&
|
|
10982
|
+
Number(entry?.stagnationEpoch || 0) === Number(stagnationEpoch || 0)
|
|
10983
|
+
);
|
|
10984
|
+
}
|
|
10985
|
+
|
|
10986
|
+
export function recordFailedCommandAttempt(toolLoop = {}, entry = {}) {
|
|
10987
|
+
if (
|
|
10988
|
+
entry?.toolName !== "run_command" ||
|
|
10989
|
+
entry?.ok !== false ||
|
|
10990
|
+
entry?.blocked === true ||
|
|
10991
|
+
entry?.noProgressProbe !== true ||
|
|
10992
|
+
!entry?.signature
|
|
10993
|
+
) {
|
|
10994
|
+
return null;
|
|
10995
|
+
}
|
|
10996
|
+
toolLoop.failedCommandAttempts = Array.isArray(toolLoop.failedCommandAttempts)
|
|
10997
|
+
? toolLoop.failedCommandAttempts
|
|
10998
|
+
: [];
|
|
10999
|
+
const prior = failedCommandAttempt(
|
|
11000
|
+
toolLoop,
|
|
11001
|
+
entry.signature,
|
|
11002
|
+
entry.stagnationEpoch
|
|
11003
|
+
);
|
|
11004
|
+
if (prior) {
|
|
11005
|
+
prior.count = Math.max(0, Number(prior.count || 0)) + 1;
|
|
11006
|
+
prior.lastOutcomeFingerprint = String(entry.outcomeFingerprint || "");
|
|
11007
|
+
prior.lastAt = String(entry.at || new Date().toISOString());
|
|
11008
|
+
} else {
|
|
11009
|
+
toolLoop.failedCommandAttempts.push({
|
|
11010
|
+
signature: entry.signature,
|
|
11011
|
+
count: 1,
|
|
11012
|
+
stagnationEpoch: Math.max(0, Number(entry.stagnationEpoch || 0)),
|
|
11013
|
+
goalRevision: Math.max(0, Number(entry.goalRevision || 0)),
|
|
11014
|
+
mutationRevision: Math.max(0, Number(entry.mutationRevision || 0)),
|
|
11015
|
+
lastOutcomeFingerprint: String(entry.outcomeFingerprint || ""),
|
|
11016
|
+
lastAt: String(entry.at || new Date().toISOString()),
|
|
11017
|
+
});
|
|
11018
|
+
}
|
|
11019
|
+
toolLoop.failedCommandAttempts = toolLoop.failedCommandAttempts.slice(-40);
|
|
11020
|
+
return prior || toolLoop.failedCommandAttempts.at(-1);
|
|
11021
|
+
}
|
|
11022
|
+
|
|
10974
11023
|
function isStaticDiscoveryToolResult(toolResult = {}) {
|
|
10975
11024
|
if (isStaticDiscoveryToolCall(toolResult.toolName, toolResult.args || {})) return true;
|
|
10976
11025
|
if (toolResult.toolName !== "run_command") return false;
|
|
@@ -11017,13 +11066,83 @@ function noProgressOutcomeFingerprint(toolResult = {}) {
|
|
|
11017
11066
|
}));
|
|
11018
11067
|
}
|
|
11019
11068
|
|
|
11069
|
+
function canvasNoProgressTarget(args = {}, config = {}) {
|
|
11070
|
+
const rawPath = String(args?.path || "").trim();
|
|
11071
|
+
if (rawPath) {
|
|
11072
|
+
const normalized = comparableOutputPath(
|
|
11073
|
+
rawPath,
|
|
11074
|
+
config.commandCwd || process.cwd()
|
|
11075
|
+
);
|
|
11076
|
+
return `path:${normalized}`;
|
|
11077
|
+
}
|
|
11078
|
+
const content = String(args?.content || "");
|
|
11079
|
+
if (content) return `content:${hashForLog(content)}`;
|
|
11080
|
+
return "";
|
|
11081
|
+
}
|
|
11082
|
+
|
|
11083
|
+
function canvasNoProgressTargetFromEntry(entry = {}, config = {}) {
|
|
11084
|
+
if (entry?.semanticSignature) return String(entry.semanticSignature);
|
|
11085
|
+
const error = String(entry?.error || "");
|
|
11086
|
+
const match = error.match(/Canvas path does not exist or is not a file:\s*(.+)$/i);
|
|
11087
|
+
if (!match?.[1]) return "";
|
|
11088
|
+
return `path:${comparableOutputPath(
|
|
11089
|
+
match[1].trim(),
|
|
11090
|
+
config.commandCwd || process.cwd()
|
|
11091
|
+
)}`;
|
|
11092
|
+
}
|
|
11093
|
+
|
|
11094
|
+
function canvasTargetExists(args = {}, config = {}) {
|
|
11095
|
+
const rawPath = String(args?.path || "").trim();
|
|
11096
|
+
if (!rawPath) return false;
|
|
11097
|
+
const candidates = path.isAbsolute(rawPath)
|
|
11098
|
+
? [rawPath]
|
|
11099
|
+
: [path.resolve(config.commandCwd || process.cwd(), rawPath)];
|
|
11100
|
+
return candidates.some((candidate) => {
|
|
11101
|
+
try {
|
|
11102
|
+
return fsSync.statSync(candidate).isFile();
|
|
11103
|
+
} catch {
|
|
11104
|
+
return false;
|
|
11105
|
+
}
|
|
11106
|
+
});
|
|
11107
|
+
}
|
|
11108
|
+
|
|
11020
11109
|
export function repeatedNoProgressToolBlock(state, toolName, args = {}, config = {}) {
|
|
11021
|
-
if (toolName !== "run_command" || expectedRepeatedObservationCommand(args.command)) return null;
|
|
11022
11110
|
const toolLoop = state.meta?.toolLoop || {};
|
|
11111
|
+
const stagnationEpoch = Math.max(0, Number(toolLoop.stagnationEpoch || 0));
|
|
11112
|
+
if (toolName === "send_to_canvas") {
|
|
11113
|
+
if (canvasTargetExists(args, config)) return null;
|
|
11114
|
+
const target = canvasNoProgressTarget(args, config);
|
|
11115
|
+
if (!target) return null;
|
|
11116
|
+
const matches = (Array.isArray(toolLoop.recent) ? toolLoop.recent : []).filter(
|
|
11117
|
+
(entry) =>
|
|
11118
|
+
entry?.toolName === "send_to_canvas" &&
|
|
11119
|
+
entry?.ok === false &&
|
|
11120
|
+
Number(entry?.stagnationEpoch || 0) === stagnationEpoch &&
|
|
11121
|
+
canvasNoProgressTargetFromEntry(entry, config) === target
|
|
11122
|
+
);
|
|
11123
|
+
if (matches.length < 2) return null;
|
|
11124
|
+
return {
|
|
11125
|
+
reason:
|
|
11126
|
+
"The same canvas artifact target already failed twice without an intervening file, artifact, browser, or task-state change.",
|
|
11127
|
+
category: "repeated-no-progress-call",
|
|
11128
|
+
permissionAdvice: {
|
|
11129
|
+
category: "repeated-no-progress-call",
|
|
11130
|
+
autoRecover: true,
|
|
11131
|
+
summary: "This is a missing-artifact convergence guard, not a permission blocker.",
|
|
11132
|
+
instruction:
|
|
11133
|
+
"Do not retry the same missing path with a different title or note. Create or repair the exact artifact with an offered tool, return its durable source path for host processing, or finish with the concrete missing-artifact blocker.",
|
|
11134
|
+
options: [
|
|
11135
|
+
"Create the exact file before sending it to canvas.",
|
|
11136
|
+
"Return a complete editable source so the host can compile the derived artifact.",
|
|
11137
|
+
"Finish with the exact missing path when no enabled tool can create it.",
|
|
11138
|
+
],
|
|
11139
|
+
},
|
|
11140
|
+
};
|
|
11141
|
+
}
|
|
11142
|
+
if (toolName !== "run_command" || expectedRepeatedObservationCommand(args.command)) return null;
|
|
11023
11143
|
const signature = staticToolCallSignature(toolName, args || {}, {
|
|
11024
11144
|
commandCwd: config.commandCwd,
|
|
11025
11145
|
});
|
|
11026
|
-
const stagnationEpoch = Math.max(0, Number(toolLoop.stagnationEpoch || 0));
|
|
11027
11146
|
const requestedCommand = projectTestCommandKey(
|
|
11028
11147
|
normalizeLeadingWorkspaceCd(args.command, config)
|
|
11029
11148
|
);
|
|
@@ -11093,6 +11212,30 @@ export function repeatedNoProgressToolBlock(state, toolName, args = {}, config =
|
|
|
11093
11212
|
Number(entry?.stagnationEpoch || 0) === stagnationEpoch &&
|
|
11094
11213
|
Boolean(entry?.outcomeFingerprint)
|
|
11095
11214
|
);
|
|
11215
|
+
const retainedFailure = failedCommandAttempt(
|
|
11216
|
+
toolLoop,
|
|
11217
|
+
signature,
|
|
11218
|
+
stagnationEpoch
|
|
11219
|
+
);
|
|
11220
|
+
if (Number(retainedFailure?.count || 0) >= 2) {
|
|
11221
|
+
return {
|
|
11222
|
+
reason:
|
|
11223
|
+
"The same command already failed twice without an intervening verified workspace, artifact, browser, or task-state change.",
|
|
11224
|
+
category: "repeated-no-progress-call",
|
|
11225
|
+
permissionAdvice: {
|
|
11226
|
+
category: "repeated-no-progress-call",
|
|
11227
|
+
autoRecover: true,
|
|
11228
|
+
summary: "This is a failed-command convergence guard, not a permission blocker.",
|
|
11229
|
+
instruction:
|
|
11230
|
+
"Do not rerun the command or a cosmetically equivalent form. Use the retained failure evidence, change the command or repair the implicated source, then run the smallest relevant validation.",
|
|
11231
|
+
options: [
|
|
11232
|
+
"Choose the correct compiler, interpreter, working directory, or command flags from the observed failure.",
|
|
11233
|
+
"Apply one bounded source repair that addresses the failure, then rerun validation.",
|
|
11234
|
+
"Finish with a concrete external blocker only when no enabled tool can make progress.",
|
|
11235
|
+
],
|
|
11236
|
+
},
|
|
11237
|
+
};
|
|
11238
|
+
}
|
|
11096
11239
|
if (matches.length < 2) return null;
|
|
11097
11240
|
const repeatedFailures = matches.slice(-2).every((entry) => entry?.ok === false);
|
|
11098
11241
|
if (repeatedFailures) {
|
|
@@ -19751,6 +19894,11 @@ async function applyToolLoopGuard(state, toolResult, store, observers, config =
|
|
|
19751
19894
|
staticDiscovery: isStaticDiscoveryToolResult(toolResult),
|
|
19752
19895
|
successfulMutation: successfulToolStateProgress(toolResult),
|
|
19753
19896
|
noProgressProbe: Boolean(outcomeFingerprint),
|
|
19897
|
+
semanticSignature:
|
|
19898
|
+
toolResult.toolName === "send_to_canvas"
|
|
19899
|
+
? canvasNoProgressTarget(toolResult.args || {}, config) ||
|
|
19900
|
+
canvasNoProgressTargetFromEntry(toolResult, config)
|
|
19901
|
+
: "",
|
|
19754
19902
|
generatedArtifactProducerAttempt: Boolean(
|
|
19755
19903
|
toolResult.toolName === "run_command" &&
|
|
19756
19904
|
config.generatedArtifactProducerPending === true &&
|
|
@@ -19772,6 +19920,7 @@ async function applyToolLoopGuard(state, toolResult, store, observers, config =
|
|
|
19772
19920
|
};
|
|
19773
19921
|
state.meta.toolLoop.recent.push(entry);
|
|
19774
19922
|
state.meta.toolLoop.recent = state.meta.toolLoop.recent.slice(-20);
|
|
19923
|
+
recordFailedCommandAttempt(state.meta.toolLoop, entry);
|
|
19775
19924
|
|
|
19776
19925
|
const activeRefresh = activePatchContextRefresh(state);
|
|
19777
19926
|
if (requiredPatchContextRefresh && !activeRefresh) {
|
|
@@ -21428,6 +21577,8 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
|
|
|
21428
21577
|
blocked: true,
|
|
21429
21578
|
reason: normalized.reason,
|
|
21430
21579
|
toolName: "send_to_canvas",
|
|
21580
|
+
args: safeArgs,
|
|
21581
|
+
category: "canvas",
|
|
21431
21582
|
};
|
|
21432
21583
|
}
|
|
21433
21584
|
|
|
@@ -21450,6 +21601,8 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
|
|
|
21450
21601
|
blocked: true,
|
|
21451
21602
|
reason: persisted.reason,
|
|
21452
21603
|
toolName: "send_to_canvas",
|
|
21604
|
+
args: safeArgs,
|
|
21605
|
+
category: "canvas",
|
|
21453
21606
|
};
|
|
21454
21607
|
}
|
|
21455
21608
|
|
package/src/command-policy.js
CHANGED
|
@@ -137,6 +137,25 @@ function isReadOnlyDiffCommand(command = "") {
|
|
|
137
137
|
);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
function isReadOnlyXargsListFilter(command = "") {
|
|
141
|
+
const normalized = stripBenignRedirections(command);
|
|
142
|
+
if (hasActiveShellExpansion(normalized)) return false;
|
|
143
|
+
const tokens = tokenizeShellWords(normalized);
|
|
144
|
+
if (!tokens.length || tokens[0] !== "xargs") return false;
|
|
145
|
+
let index = 1;
|
|
146
|
+
while (
|
|
147
|
+
["-0", "--null", "-r", "--no-run-if-empty"].includes(tokens[index])
|
|
148
|
+
) {
|
|
149
|
+
index += 1;
|
|
150
|
+
}
|
|
151
|
+
if (tokens[index] !== "ls") return false;
|
|
152
|
+
return tokens.slice(index + 1).every((token) =>
|
|
153
|
+
/^(?:-[A-Za-z0-9]+|--(?:all|almost-all|directory|inode|long|numeric-uid-gid|reverse|size|human-readable))$/.test(
|
|
154
|
+
token
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
140
159
|
function isReadOnlyFindCommand(command = "") {
|
|
141
160
|
const normalized = stripBenignRedirections(command);
|
|
142
161
|
if (!/^find\s+/.test(normalized)) return false;
|
|
@@ -209,6 +228,16 @@ function isReadOnlyFindCommand(command = "") {
|
|
|
209
228
|
return parenthesisDepth === 0 && maxDepth !== null;
|
|
210
229
|
}
|
|
211
230
|
|
|
231
|
+
function isNonMutatingFindCommand(command = "") {
|
|
232
|
+
const normalized = stripBenignRedirections(command);
|
|
233
|
+
if (!/^find\s+/.test(normalized)) return false;
|
|
234
|
+
if (/(^|\s)(-delete|-exec|-execdir|-ok|-okdir|-fprint|-fprintf|-fls)\b/.test(normalized)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
const unquoted = stripQuotedSegments(normalized);
|
|
238
|
+
return !/[|<>;&`$]/.test(unquoted) && !hasActiveShellExpansion(normalized);
|
|
239
|
+
}
|
|
240
|
+
|
|
212
241
|
function isUnboundedRecursiveGrep(command = "") {
|
|
213
242
|
const normalized = stripBenignRedirections(command);
|
|
214
243
|
if (!/^grep\s+/.test(normalized)) return false;
|
|
@@ -1860,6 +1889,7 @@ function classifySimpleCommand(normalized) {
|
|
|
1860
1889
|
isReadOnlyUniqFilter(commandForPatternMatching) ||
|
|
1861
1890
|
isReadOnlyDigestCommand(commandForPatternMatching) ||
|
|
1862
1891
|
isReadOnlyDiffCommand(commandForPatternMatching) ||
|
|
1892
|
+
isReadOnlyXargsListFilter(commandForPatternMatching) ||
|
|
1863
1893
|
isReadOnlyFindCommand(normalized) ||
|
|
1864
1894
|
(!hasActiveShellExpansion(benignRedirectCommand) && isReadOnlyShellCondition(benignRedirectCommand))
|
|
1865
1895
|
) {
|
|
@@ -1921,6 +1951,17 @@ function classifySimpleCommand(normalized) {
|
|
|
1921
1951
|
return { category: "env-setup", needsNetwork: false, writesWorkspace: true };
|
|
1922
1952
|
}
|
|
1923
1953
|
|
|
1954
|
+
if (isNonMutatingFindCommand(commandForPatternMatching)) {
|
|
1955
|
+
return {
|
|
1956
|
+
category: "general-shell",
|
|
1957
|
+
needsNetwork: false,
|
|
1958
|
+
writesWorkspace: true,
|
|
1959
|
+
semanticMayMutateProject: false,
|
|
1960
|
+
reason:
|
|
1961
|
+
"Unbounded find inspection remains under trusted shell policy but cannot mutate project state.",
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1924
1965
|
return {
|
|
1925
1966
|
category: "general-shell",
|
|
1926
1967
|
needsNetwork: false,
|
|
@@ -2770,6 +2811,13 @@ function classifyPipelineSequence(normalized) {
|
|
|
2770
2811
|
category: "general-shell",
|
|
2771
2812
|
needsNetwork: classifications.some((classification) => classification.needsNetwork),
|
|
2772
2813
|
writesWorkspace: classifications.some((classification) => classification.writesWorkspace),
|
|
2814
|
+
...(classifications.every(
|
|
2815
|
+
(classification) =>
|
|
2816
|
+
classification.category === "read-only" ||
|
|
2817
|
+
classification.semanticMayMutateProject === false
|
|
2818
|
+
)
|
|
2819
|
+
? { semanticMayMutateProject: false }
|
|
2820
|
+
: {}),
|
|
2773
2821
|
reason: `Shell pipeline includes a broad segment and requires trusted shell policy: ${normalized}`,
|
|
2774
2822
|
};
|
|
2775
2823
|
}
|