From 34d1df3eb9e46326f75a142ba31263e211e382df Mon Sep 17 00:00:00 2001 From: Jayson Grace Date: Thu, 21 May 2026 14:31:23 -0600 Subject: [PATCH 01/22] ci(renovate): enable fork processing on l50/ares Renovate skips forks by default. l50/ares is the production target for this workflow run, so opt in via RENOVATE_FORK_PROCESSING=enabled. --- .github/workflows/renovate.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index 1a375b8c..c27febed 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -77,6 +77,8 @@ jobs: RENOVATE_AUTODISCOVER: true RENOVATE_AUTODISCOVER_FILTER: "${{ github.repository }}" RENOVATE_DRY_RUN: "${{ inputs.dryRun }}" + # Required: renovate refuses to process forks unless explicitly enabled. + RENOVATE_FORK_PROCESSING: enabled RENOVATE_INTERNAL_CHECKS_FILTER: strict RENOVATE_PLATFORM: github RENOVATE_PLATFORM_COMMIT: true From b4149bd55f58b2bcd2b589bed1646d3f9066dbb5 Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:33:32 +0000 Subject: [PATCH 02/22] chore(deps): update returntocorp/semgrep docker digest to 9349edb --- .github/workflows/semgrep.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/semgrep.yaml b/.github/workflows/semgrep.yaml index a5823d85..a7189b74 100644 --- a/.github/workflows/semgrep.yaml +++ b/.github/workflows/semgrep.yaml @@ -32,7 +32,7 @@ jobs: name: 🚨 Semgrep Analysis runs-on: ubuntu-latest container: - image: returntocorp/semgrep@sha256:326e5f41cc972bb423b764a14febbb62bbad29ee1c01820805d077dd868fea48 + image: returntocorp/semgrep@sha256:9349edbadf90c3f3c0c3f55867625354e89680e6fa10d9034042af52fdb0e0d0 # Skip any PR created by dependabot to avoid permission issues: if: (github.actor != 'dependabot[bot]') From 981c9d5961fb4251d3ee5216b6040fd8c7a5e320 Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:33:40 +0000 Subject: [PATCH 03/22] chore(deps): update actions/upload-artifact action to v7.0.1 | datasource | package | from | to | | ----------- | ----------------------- | ------ | ------ | | github-tags | actions/upload-artifact | v7.0.0 | v7.0.1 | --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4996ac6a..0f3f5c09 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -86,7 +86,7 @@ jobs: done - name: Upload artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: binaries-${{ matrix.target }} path: | From e8b95f907cdd03bb1e5f913ce37ec460eefbe38f Mon Sep 17 00:00:00 2001 From: Jayson Grace Date: Thu, 21 May 2026 14:59:06 -0600 Subject: [PATCH 04/22] feat: add remote hashcat backend support (#9) **Key Changes:** - Added optional remote cracking mode that delegates hashcat jobs to an HTTP service when configured - Implemented authenticated job submission, polling, timeout handling, and potfile retrieval for remote jobs - Preserved local hashcat execution as the default path when remote service configuration is absent - Scoped remote execution to simple wordlist attacks so service-owned GPU and wordlist resources remain isolated **Added:** - Remote hashcat client module - Adds HTTP integration for submitting jobs, polling job status, retrieving cracked results, handling bearer authentication, and normalizing local wordlist paths to remote-safe basenames - Remote service configuration support - Enables remote mode through HASHCAT_SERVICE_URL and requires HASHCAT_TOKEN for authenticated requests - Remote result handling - Returns crackd logs, potfile contents, remote errors, exit codes, and timeout failures through the existing ToolOutput structure **Changed:** - Hashcat cracking flow - Updates crack_with_hashcat to check for remote service configuration first and delegate to the remote backend when available, while keeping the existing local hashcat behavior unchanged otherwise --- ares-tools/src/cracker.rs | 6 + ares-tools/src/cracker/remote.rs | 190 +++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 ares-tools/src/cracker/remote.rs diff --git a/ares-tools/src/cracker.rs b/ares-tools/src/cracker.rs index 2f9c4d3e..844441c8 100644 --- a/ares-tools/src/cracker.rs +++ b/ares-tools/src/cracker.rs @@ -7,6 +7,8 @@ use crate::args::{optional_bool, optional_i64, optional_str, required_str}; use crate::executor::CommandBuilder; use crate::ToolOutput; +mod remote; + /// Default wordlists tried in order. const DEFAULT_WORDLISTS: &[&str] = &[ "/usr/share/wordlists/rockyou.txt", @@ -88,6 +90,10 @@ fn capitalize(s: &str) -> String { /// Tries multiple wordlists in order (rockyou, seclists). When `use_dynamic_wordlist` /// is true (default), also prepends a username-derived candidate list. pub async fn crack_with_hashcat(args: &Value) -> Result { + if let Some(url) = remote::service_url() { + return remote::crack(args, &url).await; + } + let hash_value = required_str(args, "hash_value")?; let explicit_wordlist = optional_str(args, "wordlist_path"); let explicit_rules = optional_str(args, "rules_file"); diff --git a/ares-tools/src/cracker/remote.rs b/ares-tools/src/cracker/remote.rs new file mode 100644 index 00000000..0415dfdd --- /dev/null +++ b/ares-tools/src/cracker/remote.rs @@ -0,0 +1,190 @@ +//! Remote hashcat backend. +//! +//! When `HASHCAT_SERVICE_URL` (and `HASHCAT_TOKEN`) are set in the cracker +//! agent's env, [`crack_with_hashcat`](super::crack_with_hashcat) delegates to +//! an HTTP service instead of spawning hashcat locally. The remote service +//! owns the GPU and the wordlist directory; the agent becomes a thin client. +//! +//! Expected service contract: +//! - `POST /jobs` with `{hash_mode, attack_mode, hashes[], wordlist?, mask?}` +//! and `Authorization: Bearer ` → `{job_id, status}`. +//! - `GET /jobs/{id}` → `{status, log_tail?, error?}` where status is one of +//! `starting | running | done | error`. +//! - `GET /jobs/{id}/potfile` → `{cracked: [":", ...]}`. +//! +//! Scope of remote mode: wordlist attack (`-a 0`) with a single wordlist by +//! basename. Rules-based and dynamic username wordlists stay local-only — +//! the service's wordlist directory is its own concern. + +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::args::{optional_i64, optional_str, required_str}; +use crate::ToolOutput; + +use super::{detect_hashcat_mode, DEFAULT_MAX_TIME_MINUTES}; + +const DEFAULT_REMOTE_WORDLIST: &str = "rockyou.txt"; +const POLL_INTERVAL_SECS: u64 = 5; + +/// Returns the configured remote service URL, or `None` if remote mode is off. +pub(super) fn service_url() -> Option<String> { + std::env::var("HASHCAT_SERVICE_URL") + .ok() + .filter(|s| !s.is_empty()) +} + +fn service_token() -> Result<String> { + std::env::var("HASHCAT_TOKEN") + .context("HASHCAT_SERVICE_URL is set but HASHCAT_TOKEN is missing") +} + +fn http_client() -> reqwest::Client { + reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .unwrap_or_default() +} + +#[derive(Serialize)] +struct JobSubmission<'a> { + hash_mode: i64, + attack_mode: i64, + hashes: Vec<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + wordlist: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + mask: Option<&'a str>, +} + +#[derive(Deserialize)] +struct JobIdResponse { + job_id: String, +} + +#[derive(Deserialize)] +struct JobStateResponse { + status: String, + #[serde(default)] + log_tail: String, + #[serde(default)] + error: Option<String>, +} + +#[derive(Deserialize, Default)] +struct PotfileResponse { + #[serde(default)] + cracked: Vec<String>, +} + +/// Take the basename of a path. Remote services typically refuse absolute +/// paths and only accept filenames within their own wordlist directory. +fn basename(path: &str) -> String { + std::path::Path::new(path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or(path) + .to_string() +} + +pub(super) async fn crack(args: &Value, base_url: &str) -> Result<ToolOutput> { + let hash_value = required_str(args, "hash_value")?; + let token = service_token()?; + let mode = + optional_i64(args, "hashcat_mode").unwrap_or_else(|| detect_hashcat_mode(hash_value)); + let max_time_minutes = optional_i64(args, "max_time_minutes") + .unwrap_or(DEFAULT_MAX_TIME_MINUTES) + .max(DEFAULT_MAX_TIME_MINUTES); + let max_time_secs = (max_time_minutes * 60) as u64; + let wordlist = optional_str(args, "wordlist_path") + .map(basename) + .unwrap_or_else(|| DEFAULT_REMOTE_WORDLIST.to_string()); + + let client = http_client(); + let url = base_url.trim_end_matches('/'); + + let submission = JobSubmission { + hash_mode: mode, + attack_mode: 0, + hashes: vec![hash_value], + wordlist: Some(wordlist), + mask: None, + }; + + // Submit. + let job_id = { + let resp = client + .post(format!("{url}/jobs")) + .bearer_auth(&token) + .json(&submission) + .send() + .await + .context("crackd: failed to POST /jobs")?; + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + if !status.is_success() { + return Ok(ToolOutput { + stdout: String::new(), + stderr: format!("crackd submission failed ({status}): {body}"), + exit_code: Some(1), + success: false, + }); + } + serde_json::from_str::<JobIdResponse>(&body) + .context("crackd: unexpected /jobs response shape")? + .job_id + }; + + // Poll. + let started = Instant::now(); + let (terminal_status, last_log, last_error) = loop { + let resp = client + .get(format!("{url}/jobs/{job_id}")) + .bearer_auth(&token) + .send() + .await + .context("crackd: failed to GET /jobs/{id}")?; + let body = resp.text().await.unwrap_or_default(); + let state: JobStateResponse = + serde_json::from_str(&body).context("crackd: unexpected /jobs/{id} response shape")?; + if matches!(state.status.as_str(), "done" | "error") { + break (state.status, state.log_tail, state.error); + } + if started.elapsed().as_secs() > max_time_secs { + return Ok(ToolOutput { + stdout: state.log_tail, + stderr: format!("crackd job {job_id} exceeded {max_time_secs}s budget"), + exit_code: Some(124), + success: false, + }); + } + tokio::time::sleep(Duration::from_secs(POLL_INTERVAL_SECS)).await; + }; + + // Pull potfile — partial cracks are useful even on error. + let potfile: PotfileResponse = { + let resp = client + .get(format!("{url}/jobs/{job_id}/potfile")) + .bearer_auth(&token) + .send() + .await + .context("crackd: failed to GET /jobs/{id}/potfile")?; + resp.json().await.unwrap_or_default() + }; + + let stdout = format!( + "{last_log}\n--- crackd potfile ---\n{}", + potfile.cracked.join("\n") + ); + let success = terminal_status == "done"; + + Ok(ToolOutput { + stdout, + stderr: last_error.unwrap_or_default(), + exit_code: Some(if success { 0 } else { 1 }), + success, + }) +} From b012a140475c972016839b443aeae4dae25058c0 Mon Sep 17 00:00:00 2001 From: Jayson Grace <jayson.e.grace@gmail.com> Date: Thu, 21 May 2026 15:12:38 -0600 Subject: [PATCH 05/22] ci: rebuild templates when rust ares sources change (#10) --- .github/workflows/build-and-push-templates.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/build-and-push-templates.yaml b/.github/workflows/build-and-push-templates.yaml index 20ed2805..496ddb64 100644 --- a/.github/workflows/build-and-push-templates.yaml +++ b/.github/workflows/build-and-push-templates.yaml @@ -8,6 +8,15 @@ on: - 'warpgate-templates/**' - 'ansible/**' - '.github/workflows/build-and-push-templates.yaml' + # Template images bake the Rust `ares` binary from these crates; + # rebuild when their source changes too. + - 'ares-cli/**' + - 'ares-core/**' + - 'ares-llm/**' + - 'ares-rust/**' + - 'ares-tools/**' + - 'Cargo.toml' + - 'Cargo.lock' workflow_dispatch: inputs: template_filter: From a1e949701f136d099263fdcf78fc4110146e1b06 Mon Sep 17 00:00:00 2001 From: Jayson Grace <jayson.e.grace@gmail.com> Date: Thu, 21 May 2026 15:32:56 -0600 Subject: [PATCH 06/22] ci: automerge non-major renovate updates **Added:** - Renovate package rule to automerge patch and minor Cargo, Ansible Galaxy, Galaxy collection, and pre-commit updates via PR - .github/renovate.json5 --- .github/renovate.json5 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 9426f088..20604501 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -68,6 +68,21 @@ automerge: true, automergeType: 'pr', }, + { + description: 'Auto merge non-major Rust, Ansible Galaxy, and pre-commit updates', + matchManagers: [ + 'cargo', + 'galaxy', + 'galaxy-collection', + 'pre-commit', + ], + matchUpdateTypes: [ + 'patch', + 'minor', + ], + automerge: true, + automergeType: 'pr', + }, { description: 'Group opentelemetry-rust monorepo with tracing-opentelemetry so version bumps land together (tracing-opentelemetry pins a specific opentelemetry minor version, so they must update atomically)', matchPackageNames: [ From 338f346c3d6737feeb11a304daf66c1d80f791b6 Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 21:34:49 +0000 Subject: [PATCH 07/22] chore(deps): update rust crate local-ip-address to v0.6.13 (#1) | datasource | package | from | to | | ---------- | ---------------- | ------ | ------ | | crate | local-ip-address | 0.6.12 | 0.6.13 | Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- Cargo.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d82c5b15..c40e6173 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -73,7 +73,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -898,7 +898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1815,9 +1815,9 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "local-ip-address" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b0187df4e614e42405b49511b82ff7a1774fbd9a816060ee465067847cac22" +checksum = "aa08fb2b1ec3ea84575e94b489d06d4ce0cbf052d12acd515838f50e3c3d63e3" dependencies = [ "libc", "neli", @@ -1964,7 +1964,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2847,7 +2847,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2905,7 +2905,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3219,7 +3219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3534,7 +3534,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4281,7 +4281,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] From 2d71f455a6a06a0499bb69a39b5c4c1412b4b57c Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 21:35:09 +0000 Subject: [PATCH 08/22] chore(deps): update rust crate serde_json to v1.0.150 (#2) | datasource | package | from | to | | ---------- | ---------- | ------- | ------- | | crate | serde_json | 1.0.149 | 1.0.150 | Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c40e6173..1ae8850b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3023,9 +3023,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", From aeb91d4593417caa6d882c041e80c9af05ab8e83 Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 17:20:24 -0600 Subject: [PATCH 09/22] chore(deps): update dependency ansible-core to v2.21.0 (#3) | datasource | package | from | to | | ---------- | ------------ | ------ | ------ | | pypi | ansible-core | 2.20.5 | 2.21.0 | Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- .hooks/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.hooks/requirements.txt b/.hooks/requirements.txt index 5d094727..883c3ea1 100644 --- a/.hooks/requirements.txt +++ b/.hooks/requirements.txt @@ -1,4 +1,4 @@ -ansible-core==2.20.5 +ansible-core==2.21.0 ansible-lint==26.4.0 docker==7.1.0 docsible==0.8.0 From 6a7458a3d2309fc1111c9064dff8a776023de685 Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 17:20:28 -0600 Subject: [PATCH 10/22] chore(deps): update dependency ansible.posix to v2.2.0 (#4) | datasource | package | from | to | | ----------------- | ------------- | ----- | ----- | | galaxy-collection | ansible.posix | 2.1.0 | 2.2.0 | Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- ansible/requirements.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/requirements.yml b/ansible/requirements.yml index 925ac61a..c093911a 100644 --- a/ansible/requirements.yml +++ b/ansible/requirements.yml @@ -9,7 +9,7 @@ collections: - name: community.docker version: 5.2.0 - name: ansible.posix - version: 2.1.0 + version: 2.2.0 - name: community.general version: 12.6.1 - name: grafana.grafana From b5a797dafb67137fe0b91f354f6463f08e17bd43 Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 17:20:31 -0600 Subject: [PATCH 11/22] chore(deps): update rust crate sqlx to 0.9 (#5) | datasource | package | from | to | | ---------- | ------- | ----- | ----- | | crate | sqlx | 0.8.6 | 0.9.0 | Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- Cargo.lock | 327 +++++++++++++---------------------------------------- Cargo.toml | 2 +- 2 files changed, 78 insertions(+), 251 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ae8850b..3bdbddb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,7 +149,7 @@ dependencies = [ "bytes", "chrono", "futures", - "md-5 0.11.0", + "md-5", "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", @@ -515,6 +515,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "colorchoice" version = "1.0.5" @@ -683,6 +689,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -815,9 +830,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.6", "crypto-common 0.1.7", - "subtle", ] [[package]] @@ -829,6 +842,7 @@ dependencies = [ "block-buffer 0.12.0", "const-oid 0.10.2", "crypto-common 0.2.1", + "ctutils", ] [[package]] @@ -903,13 +917,12 @@ dependencies = [ [[package]] name = "etcetera" -version = "0.8.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", - "home", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -953,9 +966,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flume" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" dependencies = [ "futures-core", "futures-sink", @@ -974,6 +987,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1211,10 +1230,19 @@ name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1225,11 +1253,11 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -1316,29 +1344,20 @@ dependencies = [ [[package]] name = "hkdf" -version = "0.12.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" dependencies = [ "hmac", ] [[package]] name = "hmac" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", + "digest 0.11.3", ] [[package]] @@ -1757,9 +1776,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "leb128fmt" @@ -1779,18 +1795,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" -[[package]] -name = "libredox" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" -dependencies = [ - "bitflags", - "libc", - "plain", - "redox_syscall 0.7.5", -] - [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -1854,16 +1858,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest 0.10.7", -] - [[package]] name = "md-5" version = "0.11.0" @@ -1986,22 +1980,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.6", - "smallvec", - "zeroize", -] - [[package]] name = "num-conv" version = "0.2.1" @@ -2017,17 +1995,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -2035,7 +2002,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -2158,7 +2124,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -2294,17 +2260,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -2321,12 +2276,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "portable-atomic" version = "1.13.1" @@ -2631,15 +2580,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_syscall" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" -dependencies = [ - "bitflags", -] - [[package]] name = "regex" version = "1.12.3" @@ -2773,26 +2713,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid 0.9.6", - "digest 0.10.7", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "signature", - "spki", - "subtle", - "zeroize", -] - [[package]] name = "rstest" version = "0.26.1" @@ -3081,13 +3001,13 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3243,9 +3163,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" dependencies = [ "sqlx-core", "sqlx-macros", @@ -3256,12 +3176,13 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" dependencies = [ "base64", "bytes", + "cfg-if", "chrono", "crc", "crossbeam-queue", @@ -3271,12 +3192,11 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "hashlink", "indexmap", "log", "memchr", - "once_cell", "percent-encoding", "serde", "serde_json", @@ -3292,9 +3212,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" dependencies = [ "proc-macro2", "quote", @@ -3305,15 +3225,15 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" dependencies = [ + "cfg-if", "dotenvy", "either", "heck", "hex", - "once_cell", "proc-macro2", "quote", "serde", @@ -3324,59 +3244,44 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn", + "thiserror", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" dependencies = [ - "atoi", - "base64", "bitflags", "byteorder", "bytes", "chrono", "crc", - "digest 0.10.7", + "digest 0.11.3", "dotenvy", "either", - "futures-channel", "futures-core", - "futures-io", "futures-util", "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", "log", - "md-5 0.10.6", - "memchr", - "once_cell", "percent-encoding", - "rand 0.8.6", - "rsa", "serde", "sha1", - "sha2 0.10.9", - "smallvec", + "sha2 0.11.0", "sqlx-core", - "stringprep", "thiserror", "tracing", "uuid", - "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" dependencies = [ "atoi", "base64", @@ -3392,16 +3297,14 @@ dependencies = [ "hex", "hkdf", "hmac", - "home", "itoa", "log", - "md-5 0.10.6", + "md-5", "memchr", - "once_cell", - "rand 0.8.6", + "rand 0.10.1", "serde", "serde_json", - "sha2 0.10.9", + "sha2 0.11.0", "smallvec", "sqlx-core", "stringprep", @@ -3413,13 +3316,14 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" dependencies = [ "atoi", "chrono", "flume", + "form_urlencoded", "futures-channel", "futures-core", "futures-executor", @@ -3429,7 +3333,6 @@ dependencies = [ "log", "percent-encoding", "serde", - "serde_urlencoded", "sqlx-core", "thiserror", "tracing", @@ -4117,12 +4020,6 @@ dependencies = [ "wit-bindgen 0.51.0", ] -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.121" @@ -4261,13 +4158,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.6.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", -] +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" [[package]] name = "widestring" @@ -4354,15 +4247,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -4390,21 +4274,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4438,12 +4307,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4456,12 +4319,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4474,12 +4331,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4504,12 +4355,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4522,12 +4367,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4540,12 +4379,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4558,12 +4391,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 6a2aeeea..aa3c0c87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ anyhow = "1" clap = { version = "4.5.23", features = ["derive", "env"] } serde_yaml = "0.9" regex = "1" -sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "json", "uuid"] } +sqlx = { version = "0.9", features = ["runtime-tokio", "postgres", "chrono", "json", "uuid"] } tera = "1" hickory-resolver = { version = "0.26", default-features = false, features = ["tokio", "system-config"] } From 2df7064a1d364188cc0a0a5514ef71b5982e308f Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 17:20:35 -0600 Subject: [PATCH 12/22] chore(deps): update dependency community.general to v13 (#7) | datasource | package | from | to | | ----------------- | ----------------- | ------ | ------ | | galaxy-collection | community.general | 12.6.1 | 13.0.0 | Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- ansible/requirements.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/requirements.yml b/ansible/requirements.yml index c093911a..496a355b 100644 --- a/ansible/requirements.yml +++ b/ansible/requirements.yml @@ -11,7 +11,7 @@ collections: - name: ansible.posix version: 2.2.0 - name: community.general - version: 12.6.1 + version: 13.0.0 - name: grafana.grafana version: 6.1.0 - name: https://github.com/CowDogMoo/ansible-collection-workstation.git From a062dbf38d90b3e95427e7b25630377d7d2c3b73 Mon Sep 17 00:00:00 2001 From: Jayson Grace <jayson.e.grace@gmail.com> Date: Thu, 21 May 2026 18:29:13 -0600 Subject: [PATCH 13/22] chore: remove opentelemetry renovate version cap **Removed:** - Removed the temporary Renovate allowedVersions cap that blocked opentelemetry Rust crates from updating to 0.32 and later versions --- .github/renovate.json5 | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 20604501..5f7ff778 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -94,16 +94,6 @@ ], groupName: 'opentelemetry', }, - { - description: 'Cap opentelemetry-rust monorepo crates below 0.32 until tracing-opentelemetry ships a release that depends on opentelemetry 0.32. Without this, renovate creates a partial group PR (opentelemetry 0.32 + tracing-opentelemetry 0.32.1, which still pins opentelemetry 0.31) that fails to compile due to two opentelemetry versions in the dep graph. Remove this cap once https://crates.io/crates/tracing-opentelemetry publishes a version supporting opentelemetry 0.32.', - matchPackageNames: [ - 'opentelemetry', - 'opentelemetry_sdk', - 'opentelemetry-otlp', - 'opentelemetry-semantic-conventions', - ], - allowedVersions: '<0.32', - }, ], customManagers: [ { From 835f80e91580d04026facdc40f84ef8bd42bd6aa Mon Sep 17 00:00:00 2001 From: Jayson Grace <jayson.e.grace@gmail.com> Date: Thu, 21 May 2026 21:38:57 -0600 Subject: [PATCH 14/22] fix: assert safe dynamic sql history queries (#12) * fix: assert safety for dynamic sqlx history queries **Changed:** - Wrapped dynamically assembled history queries with `AssertSqlSafe` so sqlx accepts SQL built from static fragments with bound user values - `ares-cli/src/history` - Documented and applied the same safety assertion to credential hash search queries that construct placeholder lists dynamically - `ares-core/src/persistent_store/queries/credentials.rs` * build: update windows-sys lockfile dependency --- Cargo.lock | 2 +- ares-cli/src/history/cost.rs | 3 ++- ares-cli/src/history/list.rs | 3 ++- ares-cli/src/history/search.rs | 5 +++-- ares-core/src/persistent_store/queries/credentials.rs | 11 +++++++---- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bdbddb9..0c50362a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4174,7 +4174,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/ares-cli/src/history/cost.rs b/ares-cli/src/history/cost.rs index fcc4b7d1..511cbe2c 100644 --- a/ares-cli/src/history/cost.rs +++ b/ares-cli/src/history/cost.rs @@ -1,5 +1,6 @@ use anyhow::Result; use chrono::Utc; +use sqlx::AssertSqlSafe; use super::connect_postgres; use super::types::CostRow; @@ -36,7 +37,7 @@ pub(crate) async fn history_cost( bind_idx += 1; query.push_str(&format!(" ORDER BY started_at DESC LIMIT ${bind_idx}")); - let mut q = sqlx::query_as::<_, CostRow>(&query); + let mut q = sqlx::query_as::<_, CostRow>(AssertSqlSafe(query)); if let Some(ref d) = domain { q = q.bind(format!("%{d}%")); diff --git a/ares-cli/src/history/list.rs b/ares-cli/src/history/list.rs index 8ee154bd..b6c21e9e 100644 --- a/ares-cli/src/history/list.rs +++ b/ares-cli/src/history/list.rs @@ -1,5 +1,6 @@ use anyhow::Result; use chrono::Utc; +use sqlx::AssertSqlSafe; use super::connect_postgres; use super::types::OperationRow; @@ -48,7 +49,7 @@ pub(crate) async fn history_list( bind_idx += 1; query.push_str(&format!(" ORDER BY started_at DESC LIMIT ${bind_idx}")); - let mut q = sqlx::query_as::<_, OperationRow>(&query); + let mut q = sqlx::query_as::<_, OperationRow>(AssertSqlSafe(query)); if let Some(ref d) = domain { q = q.bind(format!("%{d}%")); diff --git a/ares-cli/src/history/search.rs b/ares-cli/src/history/search.rs index 449c639e..ed7352bb 100644 --- a/ares-cli/src/history/search.rs +++ b/ares-cli/src/history/search.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use sqlx::AssertSqlSafe; use super::connect_postgres; use super::types::{CredentialSearchRow, HashSearchRow}; @@ -40,7 +41,7 @@ pub(crate) async fn history_search_creds( bind_idx += 1; query.push_str(&format!(" ORDER BY c.created_at DESC LIMIT ${bind_idx}")); - let mut q = sqlx::query_as::<_, CredentialSearchRow>(&query); + let mut q = sqlx::query_as::<_, CredentialSearchRow>(AssertSqlSafe(query)); if let Some(ref d) = domain { q = q.bind(d); @@ -139,7 +140,7 @@ pub(crate) async fn history_search_hashes( bind_idx += 1; query.push_str(&format!(" ORDER BY h.created_at DESC LIMIT ${bind_idx}")); - let mut q = sqlx::query_as::<_, HashSearchRow>(&query); + let mut q = sqlx::query_as::<_, HashSearchRow>(AssertSqlSafe(query)); if let Some(ref d) = domain { q = q.bind(d); diff --git a/ares-core/src/persistent_store/queries/credentials.rs b/ares-core/src/persistent_store/queries/credentials.rs index 88356c7e..2a27b371 100644 --- a/ares-core/src/persistent_store/queries/credentials.rs +++ b/ares-core/src/persistent_store/queries/credentials.rs @@ -1,6 +1,7 @@ //! Credential and hash search queries across all operations. use anyhow::Result; +use sqlx::AssertSqlSafe; use super::rows::{CredentialRow, HashRow}; use super::HistoricalQueryService; @@ -198,17 +199,19 @@ impl HistoricalQueryService { ); // Bind dynamically — sqlx doesn't support dynamic binds easily, - // so we use query_scalar pattern with explicit bind count + // so we use query_scalar pattern with explicit bind count. + // SQL is built from static fragments plus $N placeholder indices only; + // user-controlled values are passed via .bind() — safe to assert. match bind_values.len() { 1 => { - sqlx::query_as::<_, HashRow>(&sql) + sqlx::query_as::<_, HashRow>(AssertSqlSafe(sql)) .bind(&bind_values[0]) .bind(limit) .fetch_all(&self.pool) .await? } 2 => { - sqlx::query_as::<_, HashRow>(&sql) + sqlx::query_as::<_, HashRow>(AssertSqlSafe(sql)) .bind(&bind_values[0]) .bind(&bind_values[1]) .bind(limit) @@ -216,7 +219,7 @@ impl HistoricalQueryService { .await? } 3 => { - sqlx::query_as::<_, HashRow>(&sql) + sqlx::query_as::<_, HashRow>(AssertSqlSafe(sql)) .bind(&bind_values[0]) .bind(&bind_values[1]) .bind(&bind_values[2]) From 6723d05621b41c6f7ed04e58ea580f951d6fefdd Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 21:51:17 -0600 Subject: [PATCH 15/22] chore(deps): update opentelemetry (#11) | datasource | package | from | to | | ---------- | ---------------------------------- | ------ | ------ | | crate | opentelemetry | 0.31.0 | 0.32.0 | | crate | opentelemetry-otlp | 0.31.1 | 0.32.0 | | crate | opentelemetry-semantic-conventions | 0.31.0 | 0.32.0 | | crate | opentelemetry_sdk | 0.31.0 | 0.32.0 | | crate | tracing-opentelemetry | 0.32.1 | 0.33.0 | Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- Cargo.lock | 116 ++++++++++++++++++++++------------------------------- Cargo.toml | 10 ++--- 2 files changed, 54 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c50362a..3823f135 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -73,7 +73,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -180,7 +180,7 @@ dependencies = [ "async-trait", "chrono", "regex", - "reqwest 0.13.3", + "reqwest", "serde", "serde_json", "tempfile", @@ -204,7 +204,7 @@ dependencies = [ "chrono", "redis", "regex", - "reqwest 0.13.3", + "reqwest", "rstest", "serde", "serde_json", @@ -912,7 +912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1448,7 +1448,6 @@ dependencies = [ "hyper", "hyper-util", "rustls", - "rustls-native-certs", "tokio", "tokio-rustls", "tower-service", @@ -1958,7 +1957,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2028,9 +2027,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "opentelemetry" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", @@ -2042,22 +2041,22 @@ dependencies = [ [[package]] name = "opentelemetry-http" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" dependencies = [ "async-trait", "bytes", "http", "opentelemetry", - "reqwest 0.12.28", + "reqwest", ] [[package]] name = "opentelemetry-otlp" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ "http", "opentelemetry", @@ -2065,18 +2064,18 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "reqwest 0.12.28", + "reqwest", "thiserror", "tokio", "tonic", - "tracing", + "tonic-types", ] [[package]] name = "opentelemetry-proto" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "opentelemetry", "opentelemetry_sdk", @@ -2087,15 +2086,16 @@ dependencies = [ [[package]] name = "opentelemetry_sdk" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +checksum = "368afaed344110f40b179bb8fbe54bc52d98f9bd2b281799ef32487c2650c956" dependencies = [ "futures-channel", "futures-executor", "futures-util", "opentelemetry", "percent-encoding", + "portable-atomic", "rand 0.9.4", "thiserror", ] @@ -2390,6 +2390,15 @@ dependencies = [ "syn", ] +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2615,46 +2624,6 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "reqwest" version = "0.13.3" @@ -2663,7 +2632,9 @@ checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64", "bytes", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "http-body-util", @@ -2767,7 +2738,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2825,7 +2796,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3139,7 +3110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3437,7 +3408,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3697,6 +3668,17 @@ dependencies = [ "tonic", ] +[[package]] +name = "tonic-types" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" +dependencies = [ + "prost", + "prost-types", + "tonic", +] + [[package]] name = "tower" version = "0.5.3" @@ -3792,9 +3774,9 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" +checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", "opentelemetry", @@ -4174,7 +4156,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index aa3c0c87..a715953f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,11 +43,11 @@ tera = "1" hickory-resolver = { version = "0.26", default-features = false, features = ["tokio", "system-config"] } # OpenTelemetry -opentelemetry = "0.31" -opentelemetry_sdk = { version = "0.31", features = ["trace"] } -opentelemetry-otlp = { version = "0.31", features = ["grpc-tonic", "http-proto", "reqwest-rustls", "trace"] } -tracing-opentelemetry = "0.32" -opentelemetry-semantic-conventions = "0.31" +opentelemetry = "0.32" +opentelemetry_sdk = { version = "0.32", features = ["trace"] } +opentelemetry-otlp = { version = "0.32", features = ["grpc-tonic", "http-proto", "reqwest-rustls", "trace"] } +tracing-opentelemetry = "0.33" +opentelemetry-semantic-conventions = "0.32" # Fast deploy profile: optimized for compile speed, acceptable runtime perf. # Use `task ec2:deploy BUILD_PROFILE=release` for production-grade optimization. From 341c173dcb66497a3a5742a726fe2d583ec05962 Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 20:55:01 -0600 Subject: [PATCH 16/22] chore(deps): update codecov/codecov-action digest to e79a696 (#13) Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- .github/workflows/rust.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 369590b7..31c7827c 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -99,7 +99,7 @@ jobs: run: cargo llvm-cov --workspace --lcov --output-path lcov.info - name: Upload coverage to Codecov - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6 with: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} From 160ad2d9ae2cd5ba0f3c7fa1feb2c3e1ea3ecbff Mon Sep 17 00:00:00 2001 From: Jayson Grace <jayson.e.grace@gmail.com> Date: Sat, 23 May 2026 21:00:00 -0600 Subject: [PATCH 17/22] fix(renovate): switch github-actions automerge from branch to pr so platformAutomerge enables GH auto-merge on PR creation --- .github/renovate.json5 | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 5f7ff778..e4550840 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -7,7 +7,6 @@ ':semanticCommits', ':enablePreCommit', ':automergeDigest', - ':automergeBranch', 'helpers:pinGitHubActionDigests', ], dependencyDashboardLabels: [ @@ -54,7 +53,7 @@ 'patch', ], automerge: true, - automergeType: 'branch', + automergeType: 'pr', }, { description: 'Auto merge warpgate patch and minor updates', @@ -68,21 +67,6 @@ automerge: true, automergeType: 'pr', }, - { - description: 'Auto merge non-major Rust, Ansible Galaxy, and pre-commit updates', - matchManagers: [ - 'cargo', - 'galaxy', - 'galaxy-collection', - 'pre-commit', - ], - matchUpdateTypes: [ - 'patch', - 'minor', - ], - automerge: true, - automergeType: 'pr', - }, { description: 'Group opentelemetry-rust monorepo with tracing-opentelemetry so version bumps land together (tracing-opentelemetry pins a specific opentelemetry minor version, so they must update atomically)', matchPackageNames: [ @@ -94,6 +78,16 @@ ], groupName: 'opentelemetry', }, + { + description: 'Cap opentelemetry-rust monorepo crates below 0.32 until tracing-opentelemetry ships a release that depends on opentelemetry 0.32. Without this, renovate creates a partial group PR (opentelemetry 0.32 + tracing-opentelemetry 0.32.1, which still pins opentelemetry 0.31) that fails to compile due to two opentelemetry versions in the dep graph. Remove this cap once https://crates.io/crates/tracing-opentelemetry publishes a version supporting opentelemetry 0.32.', + matchPackageNames: [ + 'opentelemetry', + 'opentelemetry_sdk', + 'opentelemetry-otlp', + 'opentelemetry-semantic-conventions', + ], + allowedVersions: '<0.32', + }, ], customManagers: [ { From b82de5ea9d7e0c906b5fbdba9d81efa296fa0928 Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 21:03:13 -0600 Subject: [PATCH 18/22] chore(deps): update docker/login-action digest to 650006c (#16) Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- .github/workflows/build-and-push-templates.yaml | 16 ++++++++-------- .github/workflows/test-template-builds.yaml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-and-push-templates.yaml b/.github/workflows/build-and-push-templates.yaml index 496ddb64..bd7c721a 100644 --- a/.github/workflows/build-and-push-templates.yaml +++ b/.github/workflows/build-and-push-templates.yaml @@ -517,7 +517,7 @@ jobs: fi - name: Login to GitHub Container Registry (Docker) - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -881,7 +881,7 @@ jobs: done - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -1008,7 +1008,7 @@ jobs: fi - name: Login to GitHub Container Registry (Docker) - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -1376,7 +1376,7 @@ jobs: done - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -1482,7 +1482,7 @@ jobs: fi - name: Login to GitHub Container Registry (Docker) - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -1715,7 +1715,7 @@ jobs: done - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -1817,7 +1817,7 @@ jobs: fi - name: Login to GitHub Container Registry (Docker) - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -2054,7 +2054,7 @@ jobs: done - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/test-template-builds.yaml b/.github/workflows/test-template-builds.yaml index 20a9db10..4d838b9f 100644 --- a/.github/workflows/test-template-builds.yaml +++ b/.github/workflows/test-template-builds.yaml @@ -277,7 +277,7 @@ jobs: fi - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -477,7 +477,7 @@ jobs: fi - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 with: registry: ghcr.io username: ${{ github.actor }} From 77492d0ee2b22c92621adcca3dc9b8aebf2b38c6 Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 21:12:18 -0600 Subject: [PATCH 19/22] chore(deps): update docker/setup-buildx-action digest to d7f5e7f (#17) Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- .github/workflows/build-and-push-templates.yaml | 16 ++++++++-------- .github/workflows/test-template-builds.yaml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-and-push-templates.yaml b/.github/workflows/build-and-push-templates.yaml index bd7c721a..41b276b8 100644 --- a/.github/workflows/build-and-push-templates.yaml +++ b/.github/workflows/build-and-push-templates.yaml @@ -647,7 +647,7 @@ jobs: cat ~/.config/warpgate/config.yaml - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - name: Register templates with Warpgate run: | @@ -888,7 +888,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 with: driver: docker-container @@ -1138,7 +1138,7 @@ jobs: cat ~/.config/warpgate/config.yaml - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - name: Register templates with Warpgate run: | @@ -1383,7 +1383,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 with: driver: docker-container @@ -1571,7 +1571,7 @@ jobs: EOF - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - name: Register templates with Warpgate run: | @@ -1722,7 +1722,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 with: driver: docker-container @@ -1906,7 +1906,7 @@ jobs: EOF - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 - name: Register templates with Warpgate run: | @@ -2061,7 +2061,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 with: driver: docker-container diff --git a/.github/workflows/test-template-builds.yaml b/.github/workflows/test-template-builds.yaml index 4d838b9f..ad18bad4 100644 --- a/.github/workflows/test-template-builds.yaml +++ b/.github/workflows/test-template-builds.yaml @@ -357,7 +357,7 @@ jobs: EOF - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 with: driver-opts: | image=moby/buildkit:latest @@ -557,7 +557,7 @@ jobs: EOF - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 with: driver-opts: | image=moby/buildkit:latest From 920c44f311bdb15a1e7419d8706a1e9f181e8a7b Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 21:12:25 -0600 Subject: [PATCH 20/22] chore(deps): update taiki-e/install-action digest to f48d2f8 (#18) Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- .github/workflows/rust.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 31c7827c..c7af1d71 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -79,7 +79,7 @@ jobs: components: llvm-tools-preview - name: Install cargo-llvm-cov - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2 + uses: taiki-e/install-action@f48d2f8ba2b452934c948b7be1a768079c3632ff # v2 with: tool: cargo-llvm-cov From 37a3b7b04bbe92d33a2f380129162600bc9e9eed Mon Sep 17 00:00:00 2001 From: "ares-renovate[bot]" <286782180+ares-renovate[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 21:12:33 -0600 Subject: [PATCH 21/22] chore(deps): update github/codeql-action action to v4.36.0 (#19) | datasource | package | from | to | | ----------- | -------------------- | ------- | ------- | | github-tags | github/codeql-action | v4.35.4 | v4.36.0 | Co-authored-by: ares-renovate[bot] <286782180+ares-renovate[bot]@users.noreply.github.com> --- .github/workflows/semgrep.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/semgrep.yaml b/.github/workflows/semgrep.yaml index a7189b74..d4d0c5c0 100644 --- a/.github/workflows/semgrep.yaml +++ b/.github/workflows/semgrep.yaml @@ -66,7 +66,7 @@ jobs: - name: Upload SARIF to GitHub Security tab if: always() - uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: sarif_file: semgrep-results.sarif env: From 08576d0d3057ed116d4bbaa5e7388dcc41c0b4c3 Mon Sep 17 00:00:00 2001 From: Jayson Grace <jayson.e.grace@gmail.com> Date: Tue, 26 May 2026 22:40:00 -0600 Subject: [PATCH 22/22] fix(ares-cli): unblock orchestrator bring-up (4 bugs surfaced 2026-05-26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four independent failures observed during a live op against the GOAD lab. Each is addressed in isolation; no behavioral changes outside the failing paths. 1. orchestrator double-init of tracing subscriber `ares --redis-url <url> orchestrator` panicked with `SetGlobalDefaultError`. main.rs detected the orchestrator subcommand by checking `args().nth(1)`, but global flags like --redis-url shift the subcommand later in argv, so main initialized telemetry as "ares-cli" and orchestrator::run reinitialized as "ares-orchestrator", panicking. ares-core/telemetry/init now uses `try_init()` and returns a no-op guard on a second call; ares-cli/main now scans all argv for the subcommand name. Both layers must work because either alone leaves a footgun for the next caller. Regression test in ares-core. 2. NATS tool dispatch timeout too short for nmap recon async-nats defaults `request_timeout` to 10s. The dispatcher's outer `tokio::time::timeout(1500s, client.request(...))` never got a chance to apply because the underlying NATS client raised `request timed out: deadline has elapsed` first. Bumped the NATS client request_timeout to 30 minutes at connection time and added a per-tool floor table so slow recon tools (nmap_scan, smb_sweep, password_spray, ...) get a minimum deadline that exceeds their worst-case runtime regardless of any tighter dispatcher default an operator might configure. The floor is a minimum, never a cap. 7. stale JetStream consumer blocks orchestrator restart The result-demux consumer on `ARES_TASKS` was ephemeral with no durable name; a SIGKILLed orchestrator left it behind, and the next pod hit `filtered consumer not unique on workqueue stream (10100)` on `create_consumer`. Switched to a deterministic durable name (`ares-orch-result-demux`), delete-then-create on startup, and a `inactive_threshold` of 5 min as a server-side safety net for the case where the delete itself races a pod kill. No backward-compat shim for the old ephemeral consumer — workqueue semantics make the migration idempotent. 9. org-restricted model defaults silently 403 every task `task red:multi` defaults MODEL=gpt-5.2, which OpenAI restricts to allowlisted orgs. Without access every task in the op tipped over with a non-actionable "authentication failed" string from ares-llm. ares-llm openai provider now classifies 403 + org-restriction phrasing as `LlmError::AuthError` (was generic `ApiError`), augments the error message with pointers to `OPENAI_ORG_ID` and `ARES_LLM_MODEL`, and the orchestrator runs a 1-token preflight ping right after provider creation so the failure surfaces once at startup instead of N times per submitted op. `ARES_LLM_PREFLIGHT_SKIP=1` opts out for air-gapped tests. --- ares-cli/src/main.rs | 11 +- ares-cli/src/orchestrator/mod.rs | 49 ++++++++ ares-cli/src/orchestrator/task_queue.rs | 48 ++++++++ .../src/orchestrator/tool_dispatcher/mod.rs | 38 ++++++ .../tool_dispatcher/redis_dispatcher.rs | 4 +- .../src/orchestrator/tool_dispatcher/tests.rs | 41 +++++++ ares-core/src/nats.rs | 22 +++- ares-core/src/telemetry/init.rs | 108 ++++++++++++++++-- ares-llm/src/provider/openai.rs | 78 ++++++++++++- 9 files changed, 380 insertions(+), 19 deletions(-) diff --git a/ares-cli/src/main.rs b/ares-cli/src/main.rs index 76a5f0e4..fbca77d4 100644 --- a/ares-cli/src/main.rs +++ b/ares-cli/src/main.rs @@ -58,10 +58,15 @@ async fn main() { // ── Initialize telemetry before using tracing macros ── // Skip for orchestrator/worker subcommands — they init their own telemetry - // with the correct service name. + // with the correct service name. The subcommand can appear anywhere in argv + // because clap allows global flags (e.g. `--redis-url <url>`) to precede + // it, so we scan rather than checking `args().nth(1)`. If we mis-detect, the + // telemetry init in ares-core is idempotent (`try_init`-based) and the + // redundant call returns a no-op guard, but mis-detection still bakes the + // wrong service name into spans for the entire process lifetime. let is_service_subcommand = std::env::args() - .nth(1) - .is_some_and(|a| a == "orchestrator" || a == "worker"); + .skip(1) + .any(|a| a == "orchestrator" || a == "worker"); let _telemetry = if !is_service_subcommand { Some(ares_core::telemetry::init_telemetry( ares_core::telemetry::TelemetryConfig::new("ares-cli") diff --git a/ares-cli/src/orchestrator/mod.rs b/ares-cli/src/orchestrator/mod.rs index 0d4b1c0d..337152e4 100644 --- a/ares-cli/src/orchestrator/mod.rs +++ b/ares-cli/src/orchestrator/mod.rs @@ -425,6 +425,21 @@ async fn run_inner() -> Result<()> { let (provider, model_name) = ares_llm::create_provider(&model_spec).context("Failed to create LLM provider")?; + // Fail fast on org/auth misconfigurations before queueing any tasks. A + // typical pitfall: `gpt-5.2` defaults are org-allowlisted at OpenAI, so + // submitting a multi-host op against a non-allowlisted key would silently + // burn through dispatch → LLM → 403 on every single task. A single + // pre-flight call surfaces the error once, with a hint pointing at + // `OPENAI_ORG_ID` / `ARES_LLM_MODEL`. + if let Err(e) = preflight_llm_provider(provider.as_ref(), &model_name).await { + error!( + model = %model_name, + "LLM preflight failed: {e:#} — aborting startup. Set ARES_LLM_MODEL to a widely-available model (e.g. openai/gpt-4o-mini) or ensure the org tied to the API key has access to this model." + ); + return Err(e.context(format!("LLM preflight failed for model '{model_name}'"))); + } + info!(model = %model_name, "LLM preflight ok"); + // Credential auth throttle — prevents AD account lockout by rate-limiting // auth-bearing tool calls per credential. Max 3 attempts per 30s window. // AD lockout: 3 bad attempts / 30 min. With multiple concurrent agents, @@ -891,6 +906,40 @@ async fn run_inner() -> Result<()> { Ok(()) } +/// Issue a minimal LLM chat request to verify the API key + model + org +/// permissions are good before queueing any tasks. We send a 1-token "ping" +/// so the call is cheap; the response content is discarded. A non-retryable +/// error (auth, org-restricted model, bad model name) aborts startup; a +/// retryable error (network, 5xx, rate limit) is treated as a transient +/// upstream blip and only warns. +async fn preflight_llm_provider( + provider: &dyn ares_llm::LlmProvider, + model_name: &str, +) -> Result<()> { + use ares_llm::{ChatMessage, LlmError, LlmRequest, Role}; + + // If the operator explicitly opts out (air-gapped tests, recorded + // fixtures), skip the network call. + if std::env::var("ARES_LLM_PREFLIGHT_SKIP").as_deref() == Ok("1") { + info!("ARES_LLM_PREFLIGHT_SKIP=1; skipping LLM preflight ping"); + return Ok(()); + } + + let mut req = LlmRequest::new(model_name); + req.max_tokens = 1; + req.messages.push(ChatMessage::text(Role::User, "ping")); + + match provider.chat(&req).await { + Ok(_) => Ok(()), + Err(LlmError::AuthError(msg)) => Err(anyhow::anyhow!("authentication failed: {msg}")), + Err(e) if !e.is_retryable() => Err(anyhow::anyhow!("LLM provider rejected preflight: {e}")), + Err(e) => { + warn!(err = %e, "LLM preflight returned a retryable error; continuing startup"); + Ok(()) + } + } +} + /// Run in blue-only mode: just the investigation poller, no red team. /// /// Requires only `ARES_REDIS_URL` and an LLM model. No operation ID needed. diff --git a/ares-cli/src/orchestrator/task_queue.rs b/ares-cli/src/orchestrator/task_queue.rs index 24e7f1ea..c5bc2cee 100644 --- a/ares-cli/src/orchestrator/task_queue.rs +++ b/ares-cli/src/orchestrator/task_queue.rs @@ -113,9 +113,23 @@ struct ResultDemux { } impl ResultDemux { + /// Deterministic durable name for the orchestrator's result-demux pull + /// consumer on `ARES_TASKS`. Using a fixed name (rather than an ephemeral + /// consumer) gives us a handle to delete any leftover instance from a + /// previous orchestrator incarnation before re-creating ours — a fresh + /// orchestrator otherwise hits `JetStream error: filtered consumer not + /// unique on workqueue stream (code 400, error code 10100)` on restart. + const DURABLE_NAME: &'static str = "ares-orch-result-demux"; + /// Create the consumer and spawn the drain loop. Lives for the lifetime /// of the process; the spawned task only exits if the JetStream message /// stream ends (which only happens on shutdown / connection loss). + /// + /// On `ARES_TASKS` (a WorkQueue stream) JetStream enforces that no two + /// consumers share a filter. A prior orchestrator pod that crashed (OOM, + /// SIGKILL, or eviction) leaves its consumer behind, and re-creating ours + /// fails. To stay idempotent on restart we delete any pre-existing + /// consumer with our durable name before creating a fresh one. async fn start(nats: &NatsBroker) -> Result<Arc<Self>> { use async_nats::jetstream::consumer::pull::Config as PullConfig; use async_nats::jetstream::consumer::{AckPolicy, Consumer}; @@ -126,10 +140,44 @@ impl ResultDemux { .await .with_context(|| format!("get_stream({})", nats::TASKS_STREAM))?; + // Best-effort: delete any leftover consumer from a previous incarnation. + // `delete_consumer` returns `ConsumerError::NotFound` on a clean stream; + // that's the happy path on first boot. + match stream.delete_consumer(Self::DURABLE_NAME).await { + Ok(_) => { + info!( + durable = Self::DURABLE_NAME, + "Deleted stale result-demux consumer from previous orchestrator incarnation" + ); + } + Err(e) => { + // Anything other than "not found" is logged but not fatal — if + // the next create call still trips the uniqueness check we'll + // surface that error to the caller. + let msg = e.to_string().to_lowercase(); + if msg.contains("not found") || msg.contains("consumer not found") { + // Nothing to clean up; normal first-boot path. + } else { + warn!( + durable = Self::DURABLE_NAME, + err = %e, + "Failed to delete prior result-demux consumer (continuing — create_consumer will surface the real error if any)" + ); + } + } + } + let filter = format!("{}.>", nats::TASK_RESULT_SUBJECT_PREFIX); let cfg = PullConfig { + durable_name: Some(Self::DURABLE_NAME.to_string()), + name: Some(Self::DURABLE_NAME.to_string()), filter_subject: filter.clone(), ack_policy: AckPolicy::Explicit, + // Bound how long a stale consumer can linger if we fail to clean + // it up on shutdown (best-effort delete above can race a pod kill). + // After 5 minutes of no pull requests, JetStream evicts it on its + // own and the next orchestrator can take over without manual fix-up. + inactive_threshold: Duration::from_secs(5 * 60), ..Default::default() }; let consumer: Consumer<PullConfig> = stream diff --git a/ares-cli/src/orchestrator/tool_dispatcher/mod.rs b/ares-cli/src/orchestrator/tool_dispatcher/mod.rs index 78a43ceb..5b1b460e 100644 --- a/ares-cli/src/orchestrator/tool_dispatcher/mod.rs +++ b/ares-cli/src/orchestrator/tool_dispatcher/mod.rs @@ -60,6 +60,44 @@ pub struct ToolExecResponse { /// behind another hashcat, so 2x runtime + buffer). pub(super) const DEFAULT_TOOL_TIMEOUT_SECS: u64 = 1500; +/// Tools whose worst-case runtime is materially longer than the default +/// allowance and which must not be capped at the dispatcher's generic +/// `DEFAULT_TOOL_TIMEOUT_SECS`. Maps a tool name to its minimum deadline (in +/// seconds) — the effective timeout is `max(DEFAULT_TOOL_TIMEOUT_SECS, value)` +/// so this acts as a floor, not a ceiling. Operators can still override the +/// default via `ARES_TOOL_TIMEOUT_SECS` to lift everything at once. +/// +/// Observed during the 2026-05-26 bring-up: full-port `nmap` service-version +/// scans against a Windows DC routinely take 60-180s, and `smb_sweep` / +/// `smb_signing_check` against a /24 can queue behind serialized smbclient +/// invocations. The original 10s NATS client `request_timeout` defeated even +/// the dispatcher's generous outer `tokio::time::timeout`; with the broker +/// timeout raised in `ares-core`, this table gives the dispatcher a way to +/// bump individual slow tools without touching every other code path. +pub(super) fn per_tool_timeout_floor_secs(tool_name: &str) -> Option<u64> { + match tool_name { + // nmap full-port + service version against Windows DC: ~60-180s + // observed; allow 10x headroom for slow / heavily filtered hosts. + "nmap_scan" => Some(30 * 60), + // smbclient enumeration against a /24 can serialize for minutes. + "smb_sweep" | "smb_signing_check" | "enumerate_shares" => Some(20 * 60), + // netexec-driven AD checks; chained logon attempts add up. + "domain_admin_checker" | "password_spray" | "username_as_password" => Some(20 * 60), + _ => None, + } +} + +/// Compute the dispatch deadline for a given tool. +pub(super) fn tool_timeout_for( + tool_name: &str, + default: std::time::Duration, +) -> std::time::Duration { + match per_tool_timeout_floor_secs(tool_name) { + Some(floor) if floor > default.as_secs() => std::time::Duration::from_secs(floor), + _ => default, + } +} + /// Tools that require netexec/ldapsearch and must be routed to the recon /// worker queue regardless of the calling agent's role. const RECON_ROUTED_TOOLS: &[&str] = &[ diff --git a/ares-cli/src/orchestrator/tool_dispatcher/redis_dispatcher.rs b/ares-cli/src/orchestrator/tool_dispatcher/redis_dispatcher.rs index 1c122c94..1252ba76 100644 --- a/ares-cli/src/orchestrator/tool_dispatcher/redis_dispatcher.rs +++ b/ares-cli/src/orchestrator/tool_dispatcher/redis_dispatcher.rs @@ -189,7 +189,9 @@ impl ares_llm::ToolDispatcher for RedisToolDispatcher { .context("ToolDispatcher requires NATS broker")?; let client = nats.client().clone(); - let timeout = self.tool_timeout; + // Promote slow tools (nmap, smb_*, password_spray, etc.) above the + // shared default; everything else uses the configured tool_timeout. + let timeout = super::tool_timeout_for(&call.name, self.tool_timeout); let response_msg = match tokio::time::timeout( timeout, client.request(subject.clone(), Bytes::from(payload)), diff --git a/ares-cli/src/orchestrator/tool_dispatcher/tests.rs b/ares-cli/src/orchestrator/tool_dispatcher/tests.rs index 6d6a713a..deb7fc2b 100644 --- a/ares-cli/src/orchestrator/tool_dispatcher/tests.rs +++ b/ares-cli/src/orchestrator/tool_dispatcher/tests.rs @@ -702,3 +702,44 @@ fn tool_exec_result_from_response_preserves_error_string() { assert_eq!(r.error.as_deref(), Some("connection refused")); assert!(r.discoveries.is_none()); } + +#[test] +fn tool_timeout_for_slow_recon_tools_lifts_above_small_default() { + use std::time::Duration; + // Regression for the 2026-05-26 timeout: an operator who overrode the + // dispatcher default down (or any future code path that supplies a small + // value) must still get a generous per-tool floor for nmap / smb_*. + let tiny = Duration::from_secs(60); + assert_eq!( + tool_timeout_for("nmap_scan", tiny), + Duration::from_secs(30 * 60) + ); + assert_eq!( + tool_timeout_for("smb_sweep", tiny), + Duration::from_secs(20 * 60) + ); + assert_eq!( + tool_timeout_for("password_spray", tiny), + Duration::from_secs(20 * 60) + ); +} + +#[test] +fn tool_timeout_for_unlisted_tool_uses_default() { + use std::time::Duration; + let default = Duration::from_secs(DEFAULT_TOOL_TIMEOUT_SECS); + assert_eq!(tool_timeout_for("whoami", default), default); + assert_eq!(tool_timeout_for("nslookup", default), default); +} + +#[test] +fn tool_timeout_floor_never_lowers_a_higher_caller_default() { + use std::time::Duration; + // If the dispatcher default is already above the per-tool floor (which is + // the case for `smb_sweep` and the in-tree `DEFAULT_TOOL_TIMEOUT_SECS`), + // we must not silently lower it. The floor is a minimum, not a cap. + let default = Duration::from_secs(DEFAULT_TOOL_TIMEOUT_SECS); + assert_eq!(tool_timeout_for("smb_sweep", default), default); + let huge = Duration::from_secs(60 * 60); + assert_eq!(tool_timeout_for("nmap_scan", huge), huge); +} diff --git a/ares-core/src/nats.rs b/ares-core/src/nats.rs index 2e6949c9..ac0a1fe1 100644 --- a/ares-core/src/nats.rs +++ b/ares-core/src/nats.rs @@ -149,14 +149,32 @@ pub struct NatsBroker { jetstream: JetStreamContext, } +/// Default `request_timeout` applied to the underlying `async-nats` client. +/// +/// `async-nats` defaults this to 10s, which is far too short for our tool +/// dispatch path: an `nmap` full-port scan against a Windows DC routinely +/// takes 60-180s, and `password_spray` can queue behind an auth throttle. +/// Per-call timeouts are still enforced by the dispatcher +/// (`tokio::time::timeout` around `client.request`), so the only thing this +/// value controls is the *upper bound* the NATS client will wait before +/// surfacing `request timed out: deadline has elapsed`. Set it well above +/// the longest individual tool timeout the dispatcher will impose. +const CLIENT_REQUEST_TIMEOUT_SECS: u64 = 30 * 60; + impl NatsBroker { /// Connect to NATS at the given URL (e.g. `nats://nats.attack-simulation.svc:4222`). pub async fn connect(url: &str) -> Result<Self> { - let client = async_nats::connect(url) + let client = async_nats::ConnectOptions::new() + .request_timeout(Some(Duration::from_secs(CLIENT_REQUEST_TIMEOUT_SECS))) + .connect(url) .await .with_context(|| format!("Failed to connect to NATS at {url}"))?; let jetstream = jetstream::new(client.clone()); - info!(url, "Connected to NATS"); + info!( + url, + request_timeout_secs = CLIENT_REQUEST_TIMEOUT_SECS, + "Connected to NATS" + ); Ok(Self { client, jetstream }) } diff --git a/ares-core/src/telemetry/init.rs b/ares-core/src/telemetry/init.rs index bbfeaec2..4c674094 100644 --- a/ares-core/src/telemetry/init.rs +++ b/ares-core/src/telemetry/init.rs @@ -49,17 +49,32 @@ impl TelemetryConfig { /// graceful exit to flush pending spans. pub struct TelemetryGuard { provider: Option<SdkTracerProvider>, + /// `true` when this guard is the no-op shim returned after a redundant + /// [`init_telemetry`] call. Such guards do not own a provider and must + /// not run shutdown. + already_initialized: bool, } impl TelemetryGuard { /// Flush and shut down the tracer provider. Safe to call multiple times. pub fn shutdown(&mut self) { + if self.already_initialized { + return; + } if let Some(provider) = self.provider.take() { if let Err(e) = provider.shutdown() { eprintln!("telemetry shutdown error: {e}"); } } } + + /// Returns true if this guard is a no-op shim because the tracing + /// subscriber had already been installed by a previous call. Exposed for + /// the regression test. + #[cfg(test)] + pub fn is_noop(&self) -> bool { + self.already_initialized + } } impl Drop for TelemetryGuard { @@ -96,28 +111,63 @@ pub fn init_telemetry(config: TelemetryConfig) -> TelemetryGuard { let tracer = provider.tracer(config.service_name.clone()); let otel_layer = OpenTelemetryLayer::new(tracer); - tracing_subscriber::registry() + // `try_init` returns Err if a global subscriber is already set + // (e.g. the CLI initialized one before dispatching to a long-running + // subcommand that wants its own service name). Treat that as a + // soft success: log a notice and return a no-op guard, instead of + // panicking the process at startup. + let init_result = tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) .with(otel_layer) - .init(); - - tracing::info!( - service = %config.service_name, - "telemetry initialized with OTLP exporter" - ); + .try_init(); - TelemetryGuard { - provider: Some(provider), + match init_result { + Ok(()) => { + tracing::info!( + service = %config.service_name, + "telemetry initialized with OTLP exporter" + ); + TelemetryGuard { + provider: Some(provider), + already_initialized: false, + } + } + Err(_) => { + // Subscriber already installed — discard the freshly built + // OTel provider so we don't leak a BatchSpanProcessor that + // nothing is wired into. The pre-existing subscriber stays + // authoritative for this process. + if let Err(e) = provider.shutdown() { + eprintln!("telemetry: dropped redundant provider shutdown error: {e}"); + } + tracing::debug!( + service = %config.service_name, + "telemetry already initialized by earlier call; using existing subscriber" + ); + TelemetryGuard { + provider: None, + already_initialized: true, + } + } } } None => { - tracing_subscriber::registry() + let init_result = tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) - .init(); + .try_init(); - TelemetryGuard { provider: None } + match init_result { + Ok(()) => TelemetryGuard { + provider: None, + already_initialized: false, + }, + Err(_) => TelemetryGuard { + provider: None, + already_initialized: true, + }, + } } } } @@ -204,3 +254,37 @@ fn try_init_otel_provider(service_name: &str) -> Option<SdkTracerProvider> { Some(provider) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Regression for the orchestrator double-init crash. + /// + /// Originally `init_telemetry` called `.init()` (which panics if a global + /// dispatcher is already set). Running `ares --redis-url <url> orchestrator` + /// would init once in `main` and again in `orchestrator::run`, panicking + /// with `SetGlobalDefaultError`. After the fix, the second call must + /// return a no-op `TelemetryGuard` instead of crashing the process. + #[test] + fn double_init_returns_noop_guard_instead_of_panicking() { + // First call wins and installs the subscriber. + let first = init_telemetry(TelemetryConfig::new("ares-test-first")); + // Second call must not panic; it returns a guard flagged as noop. + let second = init_telemetry(TelemetryConfig::new("ares-test-second")); + + assert!( + !first.is_noop(), + "first init_telemetry call should own the subscriber" + ); + assert!( + second.is_noop(), + "second init_telemetry call must return a no-op guard, not panic" + ); + + // Dropping the noop guard must not panic / shutdown anything; dropping + // the real guard runs the normal shutdown path. + drop(second); + drop(first); + } +} diff --git a/ares-llm/src/provider/openai.rs b/ares-llm/src/provider/openai.rs index 012d3a12..c36a27cf 100644 --- a/ares-llm/src/provider/openai.rs +++ b/ares-llm/src/provider/openai.rs @@ -248,6 +248,33 @@ fn uses_max_completion_tokens(model: &str) -> bool { model.starts_with("gpt-5") } +/// Heuristically detect OpenAI 403 messages that are caused by the API key's +/// organization not being allowlisted for the requested model. Restricted +/// models like `gpt-5.2` raise this on the *first* call, so catching it +/// cheaply lets the orchestrator fail fast with a useful hint instead of +/// letting every queued task tip over with the same opaque error. +pub(crate) fn is_org_restricted_message(msg: &str) -> bool { + let lower = msg.to_lowercase(); + lower.contains("do not have access to the organization") + || lower.contains("must be verified to use the model") + || lower.contains("not have access to model") + || lower.contains("project does not have access") +} + +/// Append a one-line operator hint to org-restricted / auth errors so the +/// failure log immediately points at the likely cause (wrong model default or +/// missing `OPENAI_ORG_ID`). Kept best-effort: if the upstream message +/// already contains a usable pointer, we don't duplicate it. +pub(crate) fn augment_org_hint(message: &str, model: &str) -> String { + let already_hinted = message.contains("OPENAI_ORG_ID") || message.contains("ARES_LLM_MODEL"); + if already_hinted { + return message.to_string(); + } + format!( + "{message} [model={model} — check OPENAI_ORG_ID and that your org is allowlisted for this model, or set ARES_LLM_MODEL to a widely-available alternative such as openai/gpt-4o-mini]" + ) +} + #[async_trait::async_trait] impl LlmProvider for OpenAiProvider { async fn chat(&self, request: &LlmRequest) -> Result<LlmResponse, LlmError> { @@ -327,7 +354,15 @@ impl LlmProvider for OpenAiProvider { return Err(match status.as_u16() { 429 => LlmError::RateLimited { retry_after_ms }, - 401 => LlmError::AuthError(message), + // 401 = bad/missing API key. 403 with org-restriction phrasing + // means the key is valid but the org isn't allowlisted for the + // requested model (typical for `gpt-5.2` and other restricted + // models). Surface both as AuthError so callers fail fast with + // a clearer message instead of treating it as a generic 4xx. + 401 => LlmError::AuthError(augment_org_hint(&message, &request.model)), + 403 if is_org_restricted_message(&message) => { + LlmError::AuthError(augment_org_hint(&message, &request.model)) + } _ => LlmError::ApiError { status: status.as_u16(), message, @@ -469,4 +504,45 @@ mod tests { assert!(uses_max_completion_tokens("openai/gpt-5.2")); assert!(!uses_max_completion_tokens("gpt-4o-mini")); } + + #[test] + fn detects_org_restricted_messages() { + // Real 403 string observed when running against a non-allowlisted org. + assert!(is_org_restricted_message( + "You do not have access to the organization tied to the API key." + )); + // Verified-org wording for gated models (currently surfaces on gpt-5.2). + assert!(is_org_restricted_message( + "Your organization must be verified to use the model `gpt-5.2`." + )); + // Project-level access denial (project-scoped API keys). + assert!(is_org_restricted_message( + "This project does not have access to model `gpt-5.2`." + )); + // Unrelated 4xx must not be classified as org-restricted. + assert!(!is_org_restricted_message( + "Invalid request: temperature out of range" + )); + assert!(!is_org_restricted_message("Rate limit exceeded")); + } + + #[test] + fn augment_org_hint_adds_actionable_pointers() { + let augmented = augment_org_hint( + "You do not have access to the organization tied to the API key.", + "gpt-5.2", + ); + assert!(augmented.contains("OPENAI_ORG_ID")); + assert!(augmented.contains("ARES_LLM_MODEL")); + assert!(augmented.contains("gpt-5.2")); + } + + #[test] + fn augment_org_hint_is_idempotent() { + // If the upstream message already mentions one of our pointers (e.g. + // operator already saw the augmented message once and re-raised it), + // we don't double up. + let pre_augmented = "Some upstream wrapper said: set OPENAI_ORG_ID"; + assert_eq!(augment_org_hint(pre_augmented, "gpt-5.2"), pre_augmented,); + } }