feat(chat): handle sudo.request / secret.request and respond via RPC (fixes #524) (#539)

Previously mobile dropped the gateway's sudo.request and secret.request
events, so any agent turn needing a sudo password or a secret value hung
forever with no UI. Now:
- EventParser maps sudo.request / secret.request -> WsEvent.SudoRequest / SecretRequest
- ChatViewModel surfaces a secure dialog (SudoPromptUi / SecretPromptUi) and
  replies via sudo.respond / secret.respond with session_id + request_id + payload
- ChatWsEventReducer explicitly branches the new events (no-op, VM-owned)
- ChatScreen renders password-masked SudoPromptDialog / SecretPromptDialog
- 6 unit tests cover prompt-state, RPC params, and prompt-clearing

Mirrors desktop/web contract (apps/shared GatewayEventName + tui_gateway).
Derives from audit of /opt/hermes-agent desktop app.
This commit is contained in:
M57 authored and GitHub committed 2026-07-10 05:00:23 +03:00
1 parent f6e6f86cc4
commit d056c77c93
8 files changed
+400

No files matched your search

@@ -115,6 +115,16 @@ object EventParser {
WsEvent.ApprovalRequest(command, description, patternKeys, sessionId)
}
"sudo.request" -> {
val requestId = payload?.get("request_id") as? String
WsEvent.SudoRequest(requestId, sessionId)
}
"secret.request" -> {
val requestId = payload?.get("request_id") as? String
WsEvent.SecretRequest(requestId, sessionId)
}
else -> {
Log.w(TAG, "Unknown event type: $eventType")
WsEvent.Unknown(rawJson)
@@ -94,6 +94,28 @@ sealed class WsEvent {
val sessionId: String?,
) : WsEvent()
// ── Sudo / secret requests ─────────────────────────────────────────
/**
* Backend needs the user's sudo password to continue a turn
* (desktop: `sudo.request` → `sudo.respond {request_id, password}`).
* Mobile previously dropped this and the agent hung forever.
*/
data class SudoRequest(
val requestId: String?,
val sessionId: String?,
) : WsEvent()
/**
* Backend needs a secret value (password / token) to continue a turn
* (desktop: `secret.request` → `secret.respond {request_id, value}`).
* Mobile previously dropped this and the agent hung forever.
*/
data class SecretRequest(
val requestId: String?,
val sessionId: String?,
) : WsEvent()
// ── Fallback ─────────────────────────────────────────────────────────
data class Unknown(
@@ -17,6 +17,8 @@ object WsMethods {
const val PROMPT_SUBMIT = "prompt.submit"
const val CLARIFY_RESPOND = "clarify.respond"
const val APPROVAL_RESPOND = "approval.respond"
const val SUDO_RESPOND = "sudo.respond"
const val SECRET_RESPOND = "secret.respond"
// ── Commands catalog ──────────────────────────────────────────────────
const val COMMANDS_CATALOG = "commands.catalog"