3434 * Detected from the agent stdio log (text pattern) and the AWF firewall audit
3535 * JSONL log (`unknown_model_ai_credits` event type). Both sources are checked
3636 * and their results merged.
37+ * - shell_expansion_guard_rejected: The sandbox's shell command-injection guard
38+ * rejected a shell command for containing (or appearing to contain) bash
39+ * expansion patterns (command substitution, indirect expansion, parameter
40+ * transformation, etc.), e.g. "...could enable arbitrary code execution.
41+ * Please rewrite the command without these expansion patterns." This can
42+ * misfire on benign multi-line `printf`/`safeoutputs` CLI invocations; agents
43+ * should switch to the `jq -Rs` file-piping pattern instead of retrying the
44+ * same command verbatim.
3745 * This replaces the individual bash scripts (detect_inference_access_error.sh,
3846 * detect_mcp_policy_error.sh) with a single JavaScript step.
3947 *
@@ -173,6 +181,16 @@ const INVOCATION_CAP_EXCEEDED_PATTERN = buildCombinedPattern(MAX_RUNS_EXCEEDED_P
173181// parseMaxCacheMissesExceededFromEventLog().
174182const MAX_CACHE_MISSES_EXCEEDED_PATTERN = / (?: \b m a x _ c a c h e _ m i s s e s _ e x c e e d e d \b | \b m a x i m u m \s + c o n s e c u t i v e \s + c a c h e \s + m i s s e s \s + e x c e e d e d \b ) / i;
175183
184+ // Pattern: the sandbox's shell command-injection guard rejected a shell command believed to
185+ // contain dangerous bash expansion patterns (command substitution, indirect expansion, parameter
186+ // transformation, backtick substitution, etc.). Observed message form:
187+ // "...indirect expansion, or nested command substitution) that could enable arbitrary code
188+ // execution. Please rewrite the command without these expansion patterns."
189+ // This guard can misfire on benign multi-line printf/safeoutputs CLI invocations. Retrying the
190+ // identical command is pointless — it will be rejected again — so this is surfaced as a distinct,
191+ // actionable diagnostic instead of a generic shell failure.
192+ const SHELL_EXPANSION_GUARD_REJECTED_PATTERN = / c o u l d e n a b l e a r b i t r a r y c o d e e x e c u t i o n \b [ ^ \n ] { 0 , 160 } \b r e w r i t e t h e c o m m a n d w i t h o u t t h e s e e x p a n s i o n p a t t e r n s \b / i;
193+
176194/**
177195 * Determines if the collected output contains the observed Copilot/CAPI quota exhaustion error.
178196 * @param {string } output - Collected stdout+stderr from the process
@@ -205,6 +223,18 @@ function isMaxCacheMissesExceededError(output) {
205223 return MAX_CACHE_MISSES_EXCEEDED_PATTERN . test ( output ) ;
206224}
207225
226+ /**
227+ * Determines if the collected output shows the sandbox's shell command-injection guard
228+ * rejected a command for containing (or appearing to contain) dangerous bash expansion
229+ * patterns. Retrying the same command verbatim will not succeed; the agent should switch
230+ * to the `jq -Rs` file-piping pattern for multi-line safeoutputs CLI bodies instead.
231+ * @param {string } output - Collected stdout+stderr from the process
232+ * @returns {boolean }
233+ */
234+ function isShellExpansionGuardRejectedError ( output ) {
235+ return SHELL_EXPANSION_GUARD_REJECTED_PATTERN . test ( output ) ;
236+ }
237+
208238/**
209239 * Normalize model names to a single safe line for GitHub Actions outputs and issue titles.
210240 * @param {string } value
@@ -227,7 +257,7 @@ function extractMissingModelPricingModelName(logContent) {
227257/**
228258 * Detect known error patterns in a log string and return detection results.
229259 * @param {string } logContent - Contents of the agent stdio log
230- * @returns {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, maxCacheMissesExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string } }
260+ * @returns {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, maxCacheMissesExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string, shellExpansionGuardRejected: boolean } }
231261 */
232262function detectErrors ( logContent ) {
233263 const missingModelPricingModelName = extractMissingModelPricingModelName ( logContent ) ;
@@ -242,12 +272,13 @@ function detectErrors(logContent) {
242272 maxCacheMissesExceeded : isMaxCacheMissesExceededError ( logContent ) ,
243273 missingModelPricingError : missingModelPricingModelName !== "" ,
244274 missingModelPricingModelName,
275+ shellExpansionGuardRejected : isShellExpansionGuardRejectedError ( logContent ) ,
245276 } ;
246277}
247278
248279/**
249280 * Build GitHub Actions output lines from detection results.
250- * @param {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, maxCacheMissesExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string } } results
281+ * @param {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, maxCacheMissesExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string, shellExpansionGuardRejected: boolean } } results
251282 * @returns {string[] }
252283 */
253284function buildOutputLines ( results ) {
@@ -263,12 +294,13 @@ function buildOutputLines(results) {
263294 `max_cache_misses_exceeded=${ results . maxCacheMissesExceeded } ` ,
264295 `missing_model_pricing_error=${ results . missingModelPricingError } ` ,
265296 `missing_model_pricing_model_name=${ results . missingModelPricingModelName } ` ,
297+ `shell_expansion_guard_rejected=${ results . shellExpansionGuardRejected } ` ,
266298 ] ;
267299}
268300
269301/**
270302 * Write GitHub Actions outputs to $GITHUB_OUTPUT.
271- * @param {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, maxCacheMissesExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string } } results
303+ * @param {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, maxCacheMissesExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string, shellExpansionGuardRejected: boolean } } results
272304 */
273305function writeOutputs ( results ) {
274306 const outputFile = process . env . GITHUB_OUTPUT ;
@@ -351,6 +383,11 @@ function main() {
351383 if ( results . missingModelPricingError && ! auditMissingPricing ) {
352384 process . stderr . write ( `[detect-agent-errors] Detected missing model pricing: model "${ results . missingModelPricingModelName } " has no AI credits pricing configured\n` ) ;
353385 }
386+ if ( results . shellExpansionGuardRejected ) {
387+ process . stderr . write (
388+ "[detect-agent-errors] Detected sandbox shell expansion guard rejection: a shell command was rejected for dangerous bash expansion patterns; use the jq -Rs file-piping pattern for multi-line safeoutputs CLI bodies instead of retrying\n"
389+ ) ;
390+ }
354391
355392 writeOutputs ( results ) ;
356393}
@@ -378,5 +415,7 @@ module.exports = {
378415 INVOCATION_CAP_EXCEEDED_PATTERN ,
379416 MAX_CACHE_MISSES_EXCEEDED_PATTERN ,
380417 MISSING_MODEL_PRICING_PATTERN ,
418+ SHELL_EXPANSION_GUARD_REJECTED_PATTERN ,
419+ isShellExpansionGuardRejectedError,
381420 buildOutputLines,
382421} ;
0 commit comments