[merge 1/5] feat(approval): debug-only OPENHUMAN_APPROVAL_TTL_SECS TTL override (#3471) (#3665)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
YellowSnnowmann
2026-06-15 18:26:54 -07:00
committed by GitHub
co-authored by Claude Fable 5 Steven Enamakel
parent 363290fcea
commit 5b80f25cd4
+87 -3
View File
@@ -219,6 +219,32 @@ impl ApprovalGate {
}
}
/// TTL for parking an approval. In debug builds `OPENHUMAN_APPROVAL_TTL_SECS`
/// overrides the boot-time default per intercept so E2E tests can exercise
/// the timeout path without waiting the full `DEFAULT_APPROVAL_TTL`.
///
/// The override is compiled out of release builds (`#[cfg(debug_assertions)]`):
/// the shipped product never reads this env var, so a hostile process
/// environment cannot shorten the supervised-mode approval window. This
/// mirrors the host-aware discipline of the `OPENHUMAN_APPROVAL_GATE`
/// kill-switch — neither override can make the gate fail open; the timeout
/// path always denies.
fn effective_ttl(&self) -> Duration {
#[cfg(debug_assertions)]
if let Some(ttl) = std::env::var("OPENHUMAN_APPROVAL_TTL_SECS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs)
{
tracing::debug!(
ttl_secs = ttl.as_secs(),
"[approval::gate] TTL env override active (debug build)"
);
return ttl;
}
self.ttl
}
/// Whether `tool_name` is on the user's "Always allow" list. Prefers the
/// process-global live policy (so a grant made this session is seen
/// immediately) and falls back to the gate's boot-time config snapshot.
@@ -409,7 +435,8 @@ impl ApprovalGate {
let request_id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now();
let expires_at = Some(now + chrono::Duration::from_std(self.ttl).unwrap_or_default());
let expires_at =
Some(now + chrono::Duration::from_std(self.effective_ttl()).unwrap_or_default());
let pending = PendingApproval {
request_id: request_id.clone(),
tool_name: tool_name.to_string(),
@@ -497,10 +524,13 @@ impl ApprovalGate {
);
// Live meetings get a clamped park window — see IN_CALL_APPROVAL_TTL.
// `effective_ttl()` applies the debug-only env override; the in-call
// clamp is applied on top so a longer override can't extend a live
// meeting's park window past IN_CALL_APPROVAL_TTL.
let effective_ttl = if in_call_ctx.is_some() {
IN_CALL_APPROVAL_TTL.min(self.ttl)
IN_CALL_APPROVAL_TTL.min(self.effective_ttl())
} else {
self.ttl
self.effective_ttl()
};
let outcome = match tokio::time::timeout(effective_ttl, rx).await {
@@ -1071,6 +1101,60 @@ mod tests {
assert!(gate.pending_for_thread("thread-42").is_none());
}
/// Tests for `effective_ttl` env-override parsing.
///
/// These run serially (they mutate the process env) via the shared
/// `TEST_ENV_LOCK`; the lock is the same one used by `auto_approve_tool_skips_prompt`
/// and the live_policy tests so they cannot clobber each other in parallel.
///
/// Guarded on `debug_assertions`: the override is compiled out of release
/// builds, so this assertion only holds under `cargo test` (debug). The
/// fallback tests below hold in either build.
#[cfg(debug_assertions)]
#[test]
fn effective_ttl_uses_env_override_when_valid() {
let _env = crate::openhuman::config::TEST_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let (gate, _dir) = test_gate(); // boot-time TTL = 2s
unsafe { std::env::set_var("OPENHUMAN_APPROVAL_TTL_SECS", "42") };
assert_eq!(
gate.effective_ttl(),
Duration::from_secs(42),
"valid OPENHUMAN_APPROVAL_TTL_SECS must override boot-time TTL"
);
unsafe { std::env::remove_var("OPENHUMAN_APPROVAL_TTL_SECS") };
}
#[test]
fn effective_ttl_falls_back_to_boot_ttl_for_garbage_value() {
let _env = crate::openhuman::config::TEST_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let (gate, _dir) = test_gate(); // boot-time TTL = 2s
unsafe { std::env::set_var("OPENHUMAN_APPROVAL_TTL_SECS", "not-a-number") };
assert_eq!(
gate.effective_ttl(),
Duration::from_secs(2),
"garbage OPENHUMAN_APPROVAL_TTL_SECS must fall back to boot-time TTL"
);
unsafe { std::env::remove_var("OPENHUMAN_APPROVAL_TTL_SECS") };
}
#[test]
fn effective_ttl_falls_back_to_boot_ttl_when_unset() {
let _env = crate::openhuman::config::TEST_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let (gate, _dir) = test_gate(); // boot-time TTL = 2s
unsafe { std::env::remove_var("OPENHUMAN_APPROVAL_TTL_SECS") };
assert_eq!(
gate.effective_ttl(),
Duration::from_secs(2),
"unset OPENHUMAN_APPROVAL_TTL_SECS must fall back to boot-time TTL"
);
}
#[test]
fn parse_approval_reply_maps_yes_no_and_rejects_other() {
for y in ["yes", "Y", " OK ", "approve", "Allow", "okay"] {