mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 16:19:21 +00:00
fix: /status ownership, model persistence scoping, heartbeat robustness
Addresses second round of PR review on #1614: - /status <job_id> DB path now validates job.user_id == requesting user before returning data (was missing ownership check, security fix). - persist_selected_model takes user_id param instead of owner_id, and skips .env/TOML writes in multi-tenant mode (these are shared global files). handle_system_command now receives user_id from caller. - JoinSet collection handles Err(JoinError) explicitly instead of silently dropping panicked tasks. - Notification forwarder extracts owner_id from response metadata in multi-tenant mode for per-user routing instead of broadcasting to the agent owner. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
+37
-17
@@ -538,30 +538,50 @@ impl Agent {
|
||||
.await;
|
||||
let notify_user = heartbeat_notify_user;
|
||||
let channels = self.channels.clone();
|
||||
let is_multi_tenant = hb_config.multi_tenant;
|
||||
tokio::spawn(async move {
|
||||
while let Some(response) = notify_rx.recv().await {
|
||||
// In multi-tenant mode, extract the owning user_id from
|
||||
// the response metadata so notifications reach the
|
||||
// correct user rather than the agent's owner.
|
||||
let effective_user = if is_multi_tenant {
|
||||
response
|
||||
.metadata
|
||||
.get("owner_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Try the configured channel first, fall back to
|
||||
// broadcasting on all channels.
|
||||
let targeted_ok = if let Some(ref channel) = notify_channel
|
||||
&& let Some(ref user) = notify_target
|
||||
{
|
||||
channels
|
||||
.broadcast(channel, user, response.clone())
|
||||
.await
|
||||
.is_ok()
|
||||
let targeted_ok = if let Some(ref channel) = notify_channel {
|
||||
let target = effective_user.as_deref().or(notify_target.as_deref());
|
||||
if let Some(user) = target {
|
||||
channels
|
||||
.broadcast(channel, user, response.clone())
|
||||
.await
|
||||
.is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !targeted_ok && let Some(ref user) = notify_user {
|
||||
let results = channels.broadcast_all(user, response).await;
|
||||
for (ch, result) in results {
|
||||
if let Err(e) = result {
|
||||
tracing::warn!(
|
||||
"Failed to broadcast heartbeat to {}: {}",
|
||||
ch,
|
||||
e
|
||||
);
|
||||
if !targeted_ok {
|
||||
let fallback = effective_user.as_deref().or(notify_user.as_deref());
|
||||
if let Some(user) = fallback {
|
||||
let results = channels.broadcast_all(user, response).await;
|
||||
for (ch, result) in results {
|
||||
if let Err(e) = result {
|
||||
tracing::warn!(
|
||||
"Failed to broadcast heartbeat to {}: {}",
|
||||
ch,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1266,7 +1286,7 @@ impl Agent {
|
||||
message.channel
|
||||
);
|
||||
// Authorization checks (including restart channel check) are enforced in handle_system_command
|
||||
self.handle_system_command(&command, &args, &message.channel)
|
||||
self.handle_system_command(&command, &args, &message.channel, &message.user_id)
|
||||
.await
|
||||
}
|
||||
Submission::Undo => self.process_undo(session, thread_id).await,
|
||||
|
||||
Reference in New Issue
Block a user