Dashboard: admin-only Close session / Kill worker

Add IDashboardSessionAdminService (Admin-role gate, friendly errors,
audit logging) wrapping a new ISessionManager.KillWorkerAsync that
skips graceful shutdown and cleans up registry/metrics. Sessions,
Workers, and SessionDetails pages render Close / Kill buttons only
when CanManage; the service re-checks the role on every call so
forged clicks return Unauthenticated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-24 07:10:32 -04:00
parent 8a0c59d7e8
commit c5e7479ee4
15 changed files with 750 additions and 1 deletions
@@ -203,6 +203,56 @@ public sealed class SessionManager : ISessionManager
return result;
}
/// <summary>
/// Forcefully terminates a session's worker without attempting graceful shutdown.
/// Mirrors the registry/metrics cleanup that <see cref="CloseSessionCoreAsync"/>
/// performs after a successful close, but skips the <c>WorkerClient.ShutdownAsync</c>
/// step that <see cref="GatewaySession.CloseAsync"/> would otherwise attempt.
/// </summary>
/// <param name="sessionId">Session identifier.</param>
/// <param name="reason">Reason recorded for the kill.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Session close result.</returns>
public async Task<SessionCloseResult> KillWorkerAsync(
string sessionId,
string reason,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(reason);
cancellationToken.ThrowIfCancellationRequested();
GatewaySession session = GetRequiredSession(sessionId);
bool wasClosed = session.State == SessionState.Closed;
try
{
session.KillWorker(reason);
}
catch (Exception exception)
{
session.MarkFaulted(exception.Message);
_metrics.Fault(SessionManagerErrorCode.CloseFailed.ToString());
await RemoveSessionAsync(session).ConfigureAwait(false);
throw new SessionManagerException(
SessionManagerErrorCode.CloseFailed,
$"Failed to kill worker for session {sessionId}.",
exception);
}
if (!wasClosed)
{
_metrics.SessionClosed();
}
await RemoveSessionAsync(session).ConfigureAwait(false);
_logger.LogInformation(
"Worker for session {SessionId} killed; reason={Reason}.",
sessionId,
reason);
return new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: wasClosed);
}
/// <summary>
/// Closes all sessions with expired leases asynchronously.
/// </summary>