Add disconnect/uninstall for persistent sessions

- Server: Add DELETE /api/sessions/:id endpoint to disconnect agents
- Server: SessionManager.disconnect_session() sends Disconnect message
- Agent: Handle ADMIN_DISCONNECT to trigger uninstall
- Agent: Add startup::uninstall() to remove from startup and schedule exe deletion
- Dashboard: Add Disconnect button in Access tab machine details
- Dashboard: Add Chat button for persistent sessions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-28 16:53:29 -07:00
parent aa03a87c7c
commit 4b29dbe6c8
7 changed files with 131 additions and 2 deletions

View File

@@ -81,6 +81,7 @@ async fn main() -> Result<()> {
// REST API - Sessions
.route("/api/sessions", get(list_sessions))
.route("/api/sessions/:id", get(get_session))
.route("/api/sessions/:id", axum::routing::delete(disconnect_session))
// HTML page routes (clean URLs)
.route("/login", get(serve_login))
@@ -178,6 +179,23 @@ async fn get_session(
Ok(Json(api::SessionInfo::from(session)))
}
async fn disconnect_session(
State(state): State<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
let session_id = match uuid::Uuid::parse_str(&id) {
Ok(id) => id,
Err(_) => return (StatusCode::BAD_REQUEST, "Invalid session ID"),
};
if state.sessions.disconnect_session(session_id, "Disconnected by administrator").await {
info!("Session {} disconnected by admin", session_id);
(StatusCode::OK, "Session disconnected")
} else {
(StatusCode::NOT_FOUND, "Session not found")
}
}
// Static page handlers
async fn serve_login() -> impl IntoResponse {
match tokio::fs::read_to_string("static/login.html").await {

View File

@@ -134,6 +134,32 @@ impl SessionManager {
}
}
/// Disconnect a session by sending a disconnect message to the agent
/// Returns true if the message was sent successfully
pub async fn disconnect_session(&self, session_id: SessionId, reason: &str) -> bool {
let sessions = self.sessions.read().await;
if let Some(session_data) = sessions.get(&session_id) {
// Create disconnect message
use crate::proto;
use prost::Message;
let disconnect_msg = proto::Message {
payload: Some(proto::message::Payload::Disconnect(proto::Disconnect {
reason: reason.to_string(),
})),
};
let mut buf = Vec::new();
if disconnect_msg.encode(&mut buf).is_ok() {
// Send via input channel (will be forwarded to agent's WebSocket)
if session_data.input_tx.send(buf).await.is_ok() {
return true;
}
}
}
false
}
/// List all active sessions
pub async fn list_sessions(&self) -> Vec<Session> {
let sessions = self.sessions.read().await;