mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-02 17:49:20 +00:00
feat: add Slack approval buttons for tool execution in DMs (#796)
* feat: add channel-relay integration for Slack via external relay service - Add RelayChannel and RelayClient for connecting to channel-relay SSE streams - Add RelayConfig with env-based configuration (CHANNEL_RELAY_URL, CHANNEL_RELAY_API_KEY) - Add channel-relay extension lifecycle: install, OAuth auth, activate with hot-add - Add proxy message sending through channel-relay for Slack chat.postMessage - Add extension registry entry for Slack relay with OAuth auth hint - Add relay integration test with mock SSE server - Wire relay channel into app startup with reconnect on stored credentials - Add AuthRequired extension error variant for cleaner auth flow detection [skip-regression-check] * chore: apply cargo fmt * fix: remove remaining Telegram test references in relay channel * fix: address PR #790 review feedback — parser handle leak, CSRF, circuit breaker - Fix parser handle leak on reconnect by sharing Arc<RwLock> instead of creating a local copy in start() (shutdown now aborts the correct task) - Add CSRF state nonce to OAuth flow: generate in auth_channel_relay, validate in slack_relay_oauth_callback_handler, one-time use - Remove dead proxy_slack method, update integration test to use proxy_provider - Add reconnect circuit breaker (max_consecutive_failures, default 50) - Fix stale docs (Telegram refs), extract event_types constants * fix: double backoff in reconnect loop and UTF-8 chunk-boundary corruption - Remove second sleep+backoff in list_connections error branch to prevent O(4^n) backoff growth (was sleeping and doubling twice per iteration) - Buffer raw bytes in SSE parser instead of per-chunk String::from_utf8_lossy to prevent U+FFFD corruption when multi-byte chars span chunk boundaries * feat: add Slack approval buttons for tool execution in DMs Send Block Kit Approve/Deny buttons via relay when a tool requires approval in a DM context. Auto-deny approval-requiring tools in shared channels to prevent prompt injection and stuck threads. * fix: address PR #796 review — use PreflightOutcome::Rejected, add tests - Auto-deny in non-DM relay channels now uses PreflightOutcome::Rejected instead of manually pushing to reason_ctx.messages, so the post-flight handler properly records the error in the turn - Add regression tests for relay auto-deny decision logic - Remove test_clean.db artifact * feat: restore Block Kit approval buttons in send_status The send_status implementation was accidentally dropped during the staging merge. Restores Approve/Deny Block Kit buttons for DM tool approval, with required sender_id validation, payload size docs, and 4 regression tests. Also removes test_clean.db. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: apply rustfmt formatting to dispatcher test code Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
8df51c04ae
commit
c94ecf19db
@@ -554,6 +554,31 @@ impl<'a> LoopDelegate for ChatDelegate<'a> {
|
||||
};
|
||||
|
||||
if needs_approval {
|
||||
// In non-DM relay channels, auto-deny approval-
|
||||
// requiring tools to prevent stuck AwaitingApproval
|
||||
// state and prompt injection from other users.
|
||||
let is_relay = self.message.channel.ends_with("-relay");
|
||||
let is_dm = self
|
||||
.message
|
||||
.metadata
|
||||
.get("event_type")
|
||||
.and_then(|v| v.as_str())
|
||||
== Some("direct_message");
|
||||
if is_relay && !is_dm {
|
||||
tracing::info!(
|
||||
tool = %tc.name,
|
||||
channel = %self.message.channel,
|
||||
"Auto-denying approval-requiring tool in non-DM relay channel"
|
||||
);
|
||||
let reject_msg = format!(
|
||||
"Tool '{}' requires approval and cannot run in shared channels. \
|
||||
Ask the user to message me directly (DM) to use this tool.",
|
||||
tc.name
|
||||
);
|
||||
preflight.push((tc, PreflightOutcome::Rejected(reject_msg)));
|
||||
continue;
|
||||
}
|
||||
|
||||
approval_needed = Some((idx, tc, tool));
|
||||
break;
|
||||
}
|
||||
@@ -2235,4 +2260,51 @@ mod tests {
|
||||
"Present 'data' field should produce non-empty string"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test the relay channel auto-deny decision logic:
|
||||
/// approval-requiring tools in non-DM relay channels must be rejected.
|
||||
#[test]
|
||||
fn test_relay_non_dm_auto_deny_decision() {
|
||||
use crate::channels::IncomingMessage;
|
||||
|
||||
// Case 1: relay channel + non-DM → should auto-deny
|
||||
let msg = IncomingMessage::new("slack-relay", "u1", "hello")
|
||||
.with_metadata(serde_json::json!({ "event_type": "message" }));
|
||||
let is_relay = msg.channel.ends_with("-relay");
|
||||
let is_dm =
|
||||
msg.metadata.get("event_type").and_then(|v| v.as_str()) == Some("direct_message");
|
||||
assert!(is_relay && !is_dm, "Should auto-deny in relay non-DM");
|
||||
|
||||
// Case 2: relay channel + DM → should NOT auto-deny
|
||||
let msg_dm = IncomingMessage::new("slack-relay", "u1", "hello")
|
||||
.with_metadata(serde_json::json!({ "event_type": "direct_message" }));
|
||||
let is_dm_2 =
|
||||
msg_dm.metadata.get("event_type").and_then(|v| v.as_str()) == Some("direct_message");
|
||||
assert!(
|
||||
!msg_dm.channel.ends_with("-relay") || is_dm_2,
|
||||
"Should NOT auto-deny in relay DM"
|
||||
);
|
||||
|
||||
// Case 3: non-relay channel → should NOT auto-deny
|
||||
let msg_web = IncomingMessage::new("web", "u1", "hello")
|
||||
.with_metadata(serde_json::json!({ "event_type": "message" }));
|
||||
assert!(
|
||||
!msg_web.channel.ends_with("-relay"),
|
||||
"Non-relay channel should not trigger auto-deny"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the auto-deny produces a PreflightOutcome::Rejected-style message.
|
||||
#[test]
|
||||
fn test_relay_auto_deny_message_format() {
|
||||
let tool_name = "shell";
|
||||
let result_msg = format!(
|
||||
"Tool '{}' requires approval and cannot run in shared channels. \
|
||||
Ask the user to message me directly (DM) to use this tool.",
|
||||
tool_name
|
||||
);
|
||||
assert!(result_msg.contains("shell"));
|
||||
assert!(result_msg.contains("approval"));
|
||||
assert!(result_msg.contains("DM"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user