Add user approval workflow for tool calls: AWAITING_APPROVAL, USER_DENIED + denialReason field #132
Labels
No labels
Epic 5
MCP
P1
PR-0
PR-1
api
api-layer
architecture
area:backend
area:frontend
arrow
authentication
backend
bug
chat
client
clipboard
common-module
compose
cross-cutting
cross-cutting-requirement
data-layer
data-models
database
desktop-client
documentation
duplicate
enhancement
epic
epic-1
epic-2
epic-3
epic-4
epic-5
epic-6
epic-7
epic-llm-config
error-handling
feature
frontend
good first issue
help wanted
implementation
infrastructure
installer
invalid
kotlin
ktor
llm
medium-priority
message
multiplatform
needs-review
needs-triage
packaging
performance
phase-1
phase-2
phase-3
phase-4
phase-5
phase3
phase:3
priority: high
question
refactor
refactoring
resource-sharing
security
server
server-module
services
session
session-management
settings
setup
shutdown
state
storage
streaming
sub-issue
tests
tooling
type:task
ui
ui/ux
user-accounts
user-management
ux
viewmodel
websocket
windows
windows-integration
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Torvian.eu/chatbot#132
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Introduce a user-approval workflow for LLM-initiated tool calls so the client can approve or deny each tool execution before it runs. When denied by the user the tool call should be marked with a dedicated status
USER_DENIEDand the optionaldenialReasonmust be stored.Background
Currently tool calls go through statuses such as
PENDING,SUCCESS, andERROR. We want the LLM to be able to request a tool call and then have the client (user) explicitly approve or deny the call. Denials should be tracked separately from execution errors.High-level changes
Add new enum values to
ToolCallStatus:AWAITING_APPROVAL— tool call saved and waiting for user approvalUSER_DENIED— user explicitly denied the tool callAdd a nullable
denialReason: String?field to theToolCallmodel and corresponding DB columndenial_reason.Add models for approval protocol:
ToolCallApprovalRequest(server→client): toolCallId, toolName, inputJson, summary of definitionToolCallApprovalResponse(client→server): toolCallId, approved: Boolean, denialReason: String?Add events to API models so server can request approval and client can respond:
ChatEvent/ChatStreamEvent: addToolCallApprovalRequested(request: ToolCallApprovalRequest)ChatClientEvent: addToolCallApproval(response: ToolCallApprovalResponse)Server flow: in
ChatServiceImpl.executeAndUpdateToolCalls(server), when encountering a tool that should be executed by server or local MCP, save it withAWAITING_APPROVALand emitToolCallApprovalRequested. Wait for a matchingToolCallApprovalResponsefrom the client's approval flow before executing. If denied, updateToolCalltoUSER_DENIEDand setdenialReason.Client flow/UI: show a
ToolCallApprovalDialogwhenToolCallApprovalRequestedis received. User can Approve or Deny (optionally provide denial reason). EmitChatClientEvent.ToolCallApprovalevent with the response. Integrate the approval response into theSendMessageUseCaseflows by merging aMutableSharedFlow<ToolCallApprovalResponse>intoclientEventsso server receives client decisions.Files & code locations (starting points)
Common models:
common/src/commonMain/kotlin/.../models/tool/ToolCallStatus.ktcommon/src/commonMain/kotlin/.../models/tool/ToolCall.ktToolCallApprovalRequest.ktandToolCallApprovalResponse.ktunder same folderAPI events:
common/src/commonMain/kotlin/.../models/api/core/ChatEvent.ktcommon/src/commonMain/kotlin/.../models/api/core/ChatStreamEvent.ktcommon/src/commonMain/kotlin/.../models/api/core/ChatClientEvent.ktServer:
server/src/main/kotlin/.../service/core/impl/ChatServiceImpl.kt— updateexecuteAndUpdateToolCallsto use approval flow and new statusesserver/src/main/kotlin/.../service/mcp/LocalMCPExecutor.kt— wrap request in approval flow (if MCP execution also needs approval)server/src/main/kotlin/.../data/tables/ToolCallTable.kt— add new columndenial_reason = text("denial_reason").nullable()server/src/main/kotlin/.../data/dao/ToolCallDao.ktandexposed/ToolCallDaoExposed.kt— update signatures/SQL mapping to includedenialReasonClient (app):
app/src/commonMain/.../viewmodel/chat/usecase/SendMessageUseCase.kt— wire up aMutableSharedFlow<ToolCallApprovalResponse>and merge it intoclientEventsfor the repositoryapp/src/commonMain/.../service/mcp/LocalMCPToolCallMediator.ktimplementations — ensure MCP flows can accept approvalsapp/src/commonMain/.../compose/chatarea/ToolCallApprovalDialog.kt(new), updateMessageItem.kt,ToolCallBadge.kt, andToolCallDetailsDialog.ktto visualize AWAITING_APPROVAL and USER_DENIED statusesDB migration notes
SQLite (Exposed): run a migration to add the
denial_reasoncolumn (nullable):ALTER TABLE tool_calls ADD COLUMN denial_reason TEXT;
No enum DB migration needed for
ToolCallStatussince the enum is stored by name; ensure the column width (currently 50) is sufficient.Testing & QA
ToolCallDaoExposedTestto assertdenialReasonis stored/updated correctly. Add tests forexecuteAndUpdateToolCallspath where approval is requested and denied/approved.ToolCallApprovalRequestedand ensure the client sendsChatClientEvent.ToolCallApprovaland server acts accordingly.Acceptance criteria
ToolCallApprovalRequestedbefore executing any tool that requires client approval.SUCCESS/ERRORdepending on execution result.USER_DENIEDanddenialReasoncontains the user's reason (or null if none). No execution occurs.Implementation notes / suggestions
Checklist
AWAITING_APPROVALandUSER_DENIEDtoToolCallStatusdenialReasontoToolCallmodel and DB tableUSER_DENIED+denialReasonfor denied tool callsLabels
["enhancement","backend","frontend"]
If you want different labels or an assignee, tell me and I can update the issue or create a follow-up.