huskies: merge 1163 story Replace perm_rx lock-as-presence-signal with a permission router
This commit is contained in:
@@ -29,14 +29,14 @@ pub(crate) async fn tool_prompt_permission(
|
||||
return Ok(json!({"behavior": "allow", "updatedInput": tool_input}).to_string());
|
||||
}
|
||||
|
||||
// Auto-deny immediately if no interactive session is currently listening on
|
||||
// perm_rx. Story 884 made the Matrix bot hold this lock for its lifetime
|
||||
// via the permission_listener task spawned at startup, so requests reach
|
||||
// chat asynchronously regardless of whether a chat message is in flight.
|
||||
// Other transports (Discord/Slack/WhatsApp) still acquire per message; if
|
||||
// none is active, try_lock succeeds — auto-deny so background agent calls
|
||||
// don't queue and flood chat at the next user session.
|
||||
if ctx.services.perm_rx.try_lock().is_ok() {
|
||||
// Auto-deny immediately if no responder is currently registered to
|
||||
// receive forwarded permission requests. The Matrix bot's
|
||||
// permission_listener task, sled uplinks, and per-message chat transports
|
||||
// all register for the duration they're able to prompt a user; if none is
|
||||
// registered, don't forward the request into the void — auto-deny so
|
||||
// background agent calls don't queue and flood chat at the next user
|
||||
// session.
|
||||
if ctx.services.permission_registry.is_empty() {
|
||||
crate::slog!(
|
||||
"[permission] Auto-denied '{tool_name}' (no interactive session — agent mode)"
|
||||
);
|
||||
@@ -141,24 +141,19 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
|
||||
// Simulate an interactive session: lock perm_rx first, signal readiness,
|
||||
// then respond with approval. The try_lock() inside tool_prompt_permission
|
||||
// must fail (lock held) so the request is forwarded rather than auto-denied.
|
||||
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
let perm_rx = ctx.services.perm_rx.clone();
|
||||
// Simulate an interactive session: register a responder first so the
|
||||
// registry is non-empty and the request is forwarded rather than
|
||||
// auto-denied, then respond with approval.
|
||||
let (guard, mut rx) = ctx.services.permission_registry.register();
|
||||
tokio::spawn(async move {
|
||||
let mut rx = perm_rx.lock().await;
|
||||
let _ = ready_tx.send(()); // signal: lock is held
|
||||
if let Some(forward) = rx.recv().await {
|
||||
let _ = forward
|
||||
.response_tx
|
||||
.send(crate::http::context::PermissionDecision::Approve);
|
||||
}
|
||||
drop(guard);
|
||||
});
|
||||
|
||||
// Wait until the spawned task holds the perm_rx lock.
|
||||
ready_rx.await.unwrap();
|
||||
|
||||
let result = tool_prompt_permission(
|
||||
&json!({"tool_name": "Bash", "input": {"command": "echo hello"}}),
|
||||
&ctx,
|
||||
@@ -182,22 +177,17 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
|
||||
// Simulate an interactive session: lock perm_rx first, then deny.
|
||||
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
let perm_rx = ctx.services.perm_rx.clone();
|
||||
// Simulate an interactive session: register a responder, then deny.
|
||||
let (guard, mut rx) = ctx.services.permission_registry.register();
|
||||
tokio::spawn(async move {
|
||||
let mut rx = perm_rx.lock().await;
|
||||
let _ = ready_tx.send(()); // signal: lock is held
|
||||
if let Some(forward) = rx.recv().await {
|
||||
let _ = forward
|
||||
.response_tx
|
||||
.send(crate::http::context::PermissionDecision::Deny);
|
||||
}
|
||||
drop(guard);
|
||||
});
|
||||
|
||||
// Wait until the spawned task holds the perm_rx lock.
|
||||
ready_rx.await.unwrap();
|
||||
|
||||
let result = tool_prompt_permission(&json!({"tool_name": "Write", "input": {}}), &ctx)
|
||||
.await
|
||||
.expect("denial must return Ok, not Err");
|
||||
|
||||
Reference in New Issue
Block a user