From 1c4b045a6afce8afaaf690eed9ab2694755235cc Mon Sep 17 00:00:00 2001 From: Michel Paulissen <862400+MichelPaulissen@users.noreply.github.com> Date: Sat, 4 Jul 2026 11:37:29 +0200 Subject: [PATCH] Report quota resource categories in CLI errors --- DISASMER_PUBLIC_TREE.json | 4 +- crates/disasmer-cli/src/main.rs | 70 +++++++++++++++++++++++++++-- scripts/cli-error-exit-smoke.js | 6 +++ scripts/cli-first-contract-smoke.js | 12 +++++ 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/DISASMER_PUBLIC_TREE.json b/DISASMER_PUBLIC_TREE.json index dcdb72f..59a544e 100644 --- a/DISASMER_PUBLIC_TREE.json +++ b/DISASMER_PUBLIC_TREE.json @@ -1,7 +1,7 @@ { "kind": "disasmer-filtered-public-tree", - "source_commit": "f4707862d419b08f13d072a31f8f5e3cb9ddda66", - "release_name": "dryrun-f4707862d419", + "source_commit": "28a8b4578c0b3fd289b938c1e6f07cc15e6848b2", + "release_name": "dryrun-28a8b4578c0b", "filtered_out": [ "private/**", "experiments/**", diff --git a/crates/disasmer-cli/src/main.rs b/crates/disasmer-cli/src/main.rs index ad67598..9d58681 100644 --- a/crates/disasmer-cli/src/main.rs +++ b/crates/disasmer-cli/src/main.rs @@ -2759,7 +2759,7 @@ fn cli_error_summary_with_default(message: &str, default_category: &'static str) } fn cli_error_summary_for_category(category: &'static str, message: &str) -> Value { - json!({ + let mut summary = json!({ "category": category, "stable_exit_code": cli_error_exit_code(category), "process_exit_code_applied": false, @@ -2767,7 +2767,56 @@ fn cli_error_summary_for_category(category: &'static str, message: &str) -> Valu "message": message, "safe_failure": true, "next_actions": cli_error_next_actions(category), - }) + }); + if category == "quota" { + if let Some(object) = summary.as_object_mut() { + object.insert( + "resource_category".to_owned(), + json!( + quota_error_resource_category(message).unwrap_or_else(|| "unknown".to_owned()) + ), + ); + object.insert("community_tier_language".to_owned(), json!(true)); + object.insert("private_abuse_heuristics_exposed".to_owned(), json!(false)); + } + } + summary +} + +fn quota_error_resource_category(message: &str) -> Option { + let lower = message.to_ascii_lowercase(); + for marker in [ + "resource limit exceeded for ", + "quota unavailable for ", + "quota exceeded for ", + "limit exceeded for ", + "metering limit exceeded for ", + "quota limit for ", + "resource category ", + "resource=", + "resource:", + ] { + if let Some(index) = lower.find(marker) { + let start = index + marker.len(); + let rest = &message[start..]; + let value: String = rest + .chars() + .skip_while(|ch| ch.is_ascii_whitespace()) + .take_while(|ch| { + ch.is_ascii_alphanumeric() + || *ch == '_' + || *ch == '-' + || *ch == '.' + || *ch == ':' + }) + .collect(); + let value = value.trim_matches(|ch: char| ch == '.' || ch == ':' || ch == ','); + if !value.is_empty() { + return Some(value.to_owned()); + } + } + } + None } fn classify_cli_error_message(message: &str) -> &'static str { @@ -5679,7 +5728,11 @@ mod tests { 20, ), ("unauthorized tenant action", "authorization", 21), - ("quota unavailable: resource limit exceeded", "quota", 22), + ( + "quota unavailable: resource limit exceeded for api_calls", + "quota", + 22, + ), ("policy denied native command execution", "policy", 23), ( "scheduler placement failed: no capable node for placement: project mismatch", @@ -5714,6 +5767,11 @@ mod tests { assert_eq!(summary["safe_failure"], true); assert_eq!(summary["process_exit_code_applied"], false); assert!(summary["next_actions"].as_array().unwrap().len() >= 2); + if category == "quota" { + assert_eq!(summary["resource_category"], "api_calls"); + assert_eq!(summary["community_tier_language"], true); + assert_eq!(summary["private_abuse_heuristics_exposed"], false); + } } } @@ -6118,6 +6176,12 @@ mod tests { assert_eq!(rejected["error_category"], "quota"); assert_eq!(rejected["stable_exit_code"], 22); assert_eq!(rejected["machine_error"]["category"], "quota"); + assert_eq!(rejected["machine_error"]["resource_category"], "api_calls"); + assert_eq!(rejected["machine_error"]["community_tier_language"], true); + assert_eq!( + rejected["machine_error"]["private_abuse_heuristics_exposed"], + false + ); assert!(rejected["machine_error"]["next_actions"] .as_array() .unwrap() diff --git a/scripts/cli-error-exit-smoke.js b/scripts/cli-error-exit-smoke.js index 18ecddb..0d811d5 100755 --- a/scripts/cli-error-exit-smoke.js +++ b/scripts/cli-error-exit-smoke.js @@ -100,6 +100,12 @@ async function main() { const report = JSON.parse(result.stdout); assert.strictEqual(report.status, "coordinator_rejected"); assert.strictEqual(report.run_start.machine_error.category, "quota"); + assert.strictEqual(report.run_start.machine_error.resource_category, "api_calls"); + assert.strictEqual(report.run_start.machine_error.community_tier_language, true); + assert.strictEqual( + report.run_start.machine_error.private_abuse_heuristics_exposed, + false + ); assert.strictEqual(report.run_start.machine_error.stable_exit_code, 22); assert.strictEqual( report.run_start.machine_error.process_exit_code_applied, diff --git a/scripts/cli-first-contract-smoke.js b/scripts/cli-first-contract-smoke.js index 15d7275..960f914 100644 --- a/scripts/cli-first-contract-smoke.js +++ b/scripts/cli-first-contract-smoke.js @@ -102,6 +102,11 @@ expect( "log redaction criteria", /Logs are capped, truncated honestly[\s\S]*preserve byte counts and truncation flags[\s\S]*Secret-like values are redacted[\s\S]*common token\/password\/bearer patterns/ ); +expect( + criteria, + "quota resource-category criteria", + /Hitting a quota produces a clear error[\s\S]*resource category[\s\S]*private abuse heuristics[\s\S]*quota machine errors now extract the resource category/ +); expect( cliFirstAcceptance, "CLI-first acceptance report", @@ -364,6 +369,11 @@ expect( "CLI classifies machine-readable error categories", /fn classify_cli_error_message[\s\S]*"authentication"[\s\S]*"authorization"[\s\S]*"quota"[\s\S]*"policy"[\s\S]*"capability"[\s\S]*"connectivity"[\s\S]*"environment"[\s\S]*"program"/ ); +expect( + cli, + "CLI exposes quota machine-error posture", + /fn cli_error_summary_for_category[\s\S]*resource_category[\s\S]*quota_error_resource_category[\s\S]*community_tier_language[\s\S]*private_abuse_heuristics_exposed[\s\S]*fn quota_error_resource_category[\s\S]*resource limit exceeded for / +); expect( cli, "CLI reports stable error-code contract", @@ -479,6 +489,8 @@ for (const [name, pattern] of [ ["fake coordinator quota rejection", /quota unavailable: resource limit exceeded for api_calls/], ["run uses JSON mode", /"run"[\s\S]*"--json"/], ["actual quota exit code", /assert\.strictEqual\(result\.code, 22/], + ["actual quota resource-category assertion", /machine_error\.resource_category, "api_calls"/], + ["actual quota abuse-heuristics assertion", /machine_error\.private_abuse_heuristics_exposed,[\s\S]*false/], ["exit-code application in JSON", /process_exit_code_applied[\s\S]*true/], ]) { expect(errorExitSmoke, name, pattern);