Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 74 additions & 10 deletions crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ use crate::filter::SubscriptionRule;
/// Override via `--idle-timeout` / `BUZZ_ACP_IDLE_TIMEOUT`.
pub(crate) const DEFAULT_IDLE_TIMEOUT_SECS: u64 = 900;

/// Default absolute wall-clock cap per agent turn (2 hours).
/// Override via `--max-turn-duration` / `BUZZ_ACP_MAX_TURN_DURATION`.
pub(crate) const DEFAULT_MAX_TURN_DURATION_SECS: u64 = 7200;

/// Upper bound for `max_turn_duration` (7 days). Any higher is operationally
/// meaningless and risks arithmetic overflow when deriving the in-flight
/// deadline (`max_turn_duration + IN_FLIGHT_DEADLINE_BUFFER_SECS`).
pub(crate) const MAX_TURN_DURATION_CEILING_SECS: u64 = 604_800;

#[derive(Debug, Error)]
pub enum ConfigError {
#[error("failed to parse nostr keys: {0}")]
Expand Down Expand Up @@ -220,7 +229,7 @@ pub struct CliArgs {
pub idle_timeout: Option<u64>,

/// Absolute wall-clock cap per turn (safety valve).
#[arg(long, env = "BUZZ_ACP_MAX_TURN_DURATION", default_value = "3600")]
#[arg(long, env = "BUZZ_ACP_MAX_TURN_DURATION", default_value_t = DEFAULT_MAX_TURN_DURATION_SECS)]
pub max_turn_duration: u64,

/// Deprecated: alias for --idle-timeout. If both set, --idle-timeout wins.
Expand Down Expand Up @@ -831,6 +840,11 @@ impl Config {
if raw == 0 {
tracing::warn!("max turn duration of 0 is invalid — using 60s minimum");
60
} else if raw > MAX_TURN_DURATION_CEILING_SECS {
return Err(ConfigError::ConfigFile(format!(
"max_turn_duration ({}s) exceeds ceiling ({}s / 7 days)",
raw, MAX_TURN_DURATION_CEILING_SECS
)));
} else {
raw
}
Expand Down Expand Up @@ -1326,7 +1340,7 @@ mod tests {
agent_args: vec!["acp".into()],
mcp_command: "".into(),
idle_timeout_secs: DEFAULT_IDLE_TIMEOUT_SECS,
max_turn_duration_secs: 3600,
max_turn_duration_secs: DEFAULT_MAX_TURN_DURATION_SECS,
agents: 1,
heartbeat_interval_secs: 0,
turn_liveness_secs: 10,
Expand Down Expand Up @@ -2231,7 +2245,7 @@ channels = "ALL"
"summary should include {expected_idle}: {summary}"
);
assert!(
summary.contains("max_turn=3600s"),
summary.contains(&format!("max_turn={DEFAULT_MAX_TURN_DURATION_SECS}s")),
"summary should include max_turn: {summary}"
);
}
Expand Down Expand Up @@ -2436,13 +2450,10 @@ channels = "ALL"
"test precondition: idle must be >= max_turn to trigger guard"
);

// And the valid case:
let idle_valid = 900u64;
let max_turn_valid = 3600u64;
assert!(
idle_valid < max_turn_valid,
"default idle (900) must be less than default max_turn (3600)"
);
// And the valid case (const assertion so clippy doesn't flag it):
const {
assert!(DEFAULT_IDLE_TIMEOUT_SECS < DEFAULT_MAX_TURN_DURATION_SECS);
}
}

// --- BUZZ_ACP_ALLOWED_RESPOND_TO gate ---
Expand Down Expand Up @@ -2627,4 +2638,57 @@ channels = "ALL"
"from_args should accept any mode when allowed list is unset: {result:?}"
);
}

// --- max_turn_duration ceiling gate ---

#[test]
fn max_turn_duration_at_ceiling_is_accepted() {
let args = CliArgs::try_parse_from([
"buzz-acp",
"--private-key",
TEST_PRIVATE_KEY,
"--max-turn-duration",
&MAX_TURN_DURATION_CEILING_SECS.to_string(),
])
.expect("clap should parse args");
let result = Config::from_args(args);

assert!(
result.is_ok(),
"from_args should accept max_turn_duration at the ceiling: {result:?}"
);
}

#[test]
fn max_turn_duration_above_ceiling_is_rejected() {
let over = MAX_TURN_DURATION_CEILING_SECS + 1;
let args = CliArgs::try_parse_from([
"buzz-acp",
"--private-key",
TEST_PRIVATE_KEY,
"--max-turn-duration",
&over.to_string(),
])
.expect("clap should parse args");
let result = Config::from_args(args);

assert!(
result.is_err(),
"from_args should reject max_turn_duration above the ceiling"
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("exceeds ceiling"),
"error should mention 'exceeds ceiling': {msg}"
);
}

#[test]
fn max_turn_duration_ceiling_cannot_overflow_in_flight_deadline() {
// The in-flight deadline is max_turn + 100s buffer (IN_FLIGHT_DEADLINE_BUFFER_SECS).
// Verify that even at the ceiling, this addition cannot overflow u64.
const {
assert!(MAX_TURN_DURATION_CEILING_SECS < u64::MAX - 100);
}
}
}
Loading