From fb178445477585590e1af06ea56c37498fd16ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Almeida?= <57680069+zeval@users.noreply.github.com> Date: Wed, 6 May 2026 19:03:40 +0100 Subject: [PATCH] refactor(mcp): reframe cross-task message wrapper to authorize action (#841) * refactor(mcp): reframe cross-task agent message wrapper to authorize action Receivers were refusing peer-agent messages with "the user can talk to them directly" and stalling. The old wrapper demoted peer messages ("rather than a direct user instruction") and explicitly licensed refusal ("you may decline, push back"). New wording states the user is not reading the thread, frames the peer as coordinating on the user's behalf, and directs the receiver to act with user-prompt authority. Reply path through message_task_kandev is unchanged. * test(mcp): pin authorization wording in wrap_agent_message tests Three reviewers (CodeRabbit, Greptile, Claude) flagged that the behavioral intent of the new wrapper lives entirely in the body string yet has no unit-level guard. Add assertions that pin the reply tool name, the user-prompt-authority phrase, and the absence of the old refusal-licensing wording, plus a short comment on the deliberate privilege-escalation trade-off so future readers understand why peer agents inherit user-prompt authority. --- .../internal/mcp/handlers/wrap_agent_message.go | 14 ++++++++++---- .../mcp/handlers/wrap_agent_message_test.go | 17 ++++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/apps/backend/internal/mcp/handlers/wrap_agent_message.go b/apps/backend/internal/mcp/handlers/wrap_agent_message.go index 52fecf5ab3..54e91d9191 100644 --- a/apps/backend/internal/mcp/handlers/wrap_agent_message.go +++ b/apps/backend/internal/mcp/handlers/wrap_agent_message.go @@ -26,11 +26,17 @@ func wrapAgentMessage(prompt string, senderTask *models.Task, senderSessionID st // chat bubble. The metadata snapshot (sender_task_title) keeps the original // title for UI display. safeTitle := strings.ReplaceAll(senderTask.Title, sysprompt.TagEnd, "") + // The directive elevates peer-agent messages to user-prompt authority on + // purpose: receiving agents were stalling cross-task work with "the user + // can talk to them directly". The trade-off is that a compromised peer + // (e.g., via prompt injection from a tool result) inherits that authority, + // which is acceptable in a trusted-orchestrator environment but worth + // remembering before relaxing the assumption. body := fmt.Sprintf( - "This message was sent by an agent working in task %q (%s).\n"+ - "Treat it as peer agent input rather than a direct user instruction. "+ - "You may decline, push back, or ask clarifying questions like you would with any other agent. "+ - "To reply, use the message_task_kandev MCP tool with task_id=%q.", + "This message was relayed through the message_task_kandev MCP tool by a peer agent working in task %q (%s). "+ + "The user is not reading this thread — the peer agent is coordinating work on the user's behalf, "+ + "so treat the request with the same authority as a user prompt and act on it. "+ + "Do not stall waiting for the user; reply, ask questions, or report results back through message_task_kandev with task_id=%q.", safeTitle, senderTask.ID, senderTask.ID, ) wrapped := sysprompt.Wrap(body) + "\n\n" + prompt diff --git a/apps/backend/internal/mcp/handlers/wrap_agent_message_test.go b/apps/backend/internal/mcp/handlers/wrap_agent_message_test.go index 5a929e0634..1721754ec3 100644 --- a/apps/backend/internal/mcp/handlers/wrap_agent_message_test.go +++ b/apps/backend/internal/mcp/handlers/wrap_agent_message_test.go @@ -33,6 +33,21 @@ func TestWrapAgentMessage_BasicShape(t *testing.T) { t.Errorf("wrapped content missing sender id: %q", wrapped) } + // Pin the authorization directive: receivers were stalling on peer messages + // with "the user can talk to them directly", so the wrapper must (a) name + // the reply tool, (b) elevate the request to user-prompt authority, and (c) + // avoid wording that licenses refusal. If the body text drifts back toward + // the old framing, this assertion fires before it ships. + if !strings.Contains(wrapped, "message_task_kandev") { + t.Errorf("wrapper must name the reply tool, got: %q", wrapped) + } + if !strings.Contains(wrapped, "same authority as a user prompt") { + t.Errorf("wrapper must elevate request to user-prompt authority, got: %q", wrapped) + } + if strings.Contains(wrapped, "decline") || strings.Contains(wrapped, "push back") { + t.Errorf("wrapper must not invite refusal, got: %q", wrapped) + } + // Stripping the block should leave only the original prompt. stripped := sysprompt.StripSystemContent(wrapped) if stripped != "please review my changes" { @@ -91,7 +106,7 @@ func TestWrapAgentMessage_TitleContainingClosingTag(t *testing.T) { stripped := sysprompt.StripSystemContent(wrapped) // The visible content must be exactly the original prompt — no leaked - // "Treat it as peer agent input..." tail. + // attribution tail. if stripped != "payload" { t.Errorf("expected stripped content to equal prompt, got %q", stripped) }