Skip to content

Add experimental AskAnotherUser tool with disclosure and cancel controls - #945

Open
crspeller wants to merge 20 commits into
masterfrom
cursor/agent-ask-user-tool-5d96
Open

Add experimental AskAnotherUser tool with disclosure and cancel controls#945
crspeller wants to merge 20 commits into
masterfrom
cursor/agent-ask-user-tool-5d96

Conversation

@crspeller

@crspeller crspeller commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Adds an experimental, default-off AskAnotherUser capability that lets an Agent ask another Mattermost user a clarifying question, wait for the response, and continue the originating task.

Admins enable it in System Console → Agents → AI Functions → Enable Agents to Ask Other Users. The setting explains the implications: bot-authored DMs to users who did not initiate them, answers flowing back to the originating DM/channel, and the additional prompt-injection/social-engineering surface.

Key behavior:

  • Deferred-result built-in tool governed by existing ask / auto_run_in_dm / auto_run_everywhere policy; default behavior requires initiator approval.
  • Mattermost-styled target card supporting multiple choice, free-form answers, and Decline.
  • Server-authored attribution and destination disclosure before answering:
    • DM: Your answer will be shared with @requester.
    • Channel: Your answer may be shared with the N members of ~channel.
    • Requester name/position and available channel ABAC-policy context are shown when the server provides them; unavailable policy context is omitted.
  • Model-authored question/context/options are contained in a distinct AI-generated content region. Reserved system phrases are stripped/rejected server-side so model text cannot forge attribution/disclosure lines.
  • Autonomous runs explicitly render Asked by the <agent> agent running unattended (no human requester).
  • Initiators can select Cancel question while waiting. The Agent resumes with a structured canceled result (answer_received:false, canceled_by:"requester"); the target card becomes This question is no longer needed.
  • Late answers after cancel are successful HTTP 200 no-ops ({"status":"canceled"}): no second tool result or continuation. Duplicate normal answers remain 409.
  • State stays in LLM_Turns JSON + post props; no schema migration.

Security/reliability:

  • Target-only answer and initiator-only cancel authorization at API and service layers.
  • Atomic per-tool-use claims across HA nodes for dispatch and resolution, recording the winning answer/decline/cancel value to close pre-persistence races.
  • Plaintext fallback includes requester attribution; generated fields have rune limits.
  • Destination names, member counts, requester identity, and policy flags are server-computed, never model-authored.
  • Async continuations detach request cancellation while preserving audit-record enrichment.

Testing:

  • Full merged-master make check passes with Go 1.26.5: 4,499 Go tests (85 skipped), 500 Jest tests, lint/type-check/shard/i18n/lock/go-mod checks clean.
  • Playwright two-user spec covers answer, decline, disclosure/AI-region separation, and cancel (3/3 green).
  • Independent backend/webapp reviews completed; all material findings remediated and re-reviewed.
  • Live Mattermost + Anthropic QA verified toggle ON/OFF, tool absence while OFF, DM answer/resume, channel member disclosure, requester identity context, anti-impersonation stripping, cancel/no-answer behavior, and late-answer HTTP 200 with no second continuation.

QA steps:

  1. Enable Enable Agents to Ask Other Users in System Console → Agents → AI Functions.
  2. Ask an Agent to ask another user a specific question and approve AskAnotherUser.
  3. As the target, verify attribution, destination/access disclosure, and the AI-generated-content boundary before answering/declining.
  4. Verify answer/decline resumes the original task.
  5. Repeat and select Cancel question; verify the target card becomes no-longer-needed and the Agent continues without attributing a decline to the target.

Ticket Link

NONE

Screenshots

Experimental toggle and implications:

Experimental AskAnotherUser system-console toggle and implications

Target card with attribution, AI-generated-content boundary, DM disclosure, and requester access context:

Target card showing Alice Anderson and Incident Commander access context

Channel destination disclosure:

Question card disclosing three Town Square recipients

Anti-impersonation rendering (attempted forged system text is absent from the AI region):

AI-generated question visually separated from system attribution

Cancel walkthrough (approval → waiting → cancel → explicit no-answer continuation):

ask_user_v2_cancel_walkthrough_final_20260819.mp4

Release Note

Added an experimental, default-off AskAnotherUser Agent capability with an admin toggle, requester/destination/access-context disclosure, anti-impersonation rendering, target answer/decline controls, and initiator cancellation. Added POST /plugins/mattermost-ai/post/{post_id}/ask_user_response and POST /plugins/mattermost-ai/post/{post_id}/ask_user_cancel endpoints, the enableAskAnotherUser config setting, configurable built-in tool policies, and a conversation_updated plugin websocket event.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • Added interactive “Ask another user” questions with selectable, multi-select, free-form, and decline responses.
    • Added question cards with requester details, response status, answer previews, and links to the originating conversation.
    • Added deferred AI tool results with waiting states and automatic conversation continuation.
    • Added administrator controls for built-in tool policies.
  • Bug Fixes
    • Improved handling of failed, pending, declined, and invalid responses with clearer status updates and error handling.

cursoragent and others added 2 commits August 3, 2026 05:42
…swer endpoint

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🤖 LLM Evaluation Results

OpenAI

⚠️ Overall: 21/28 tests passed (75.0%)

Provider Total Passed Failed Pass Rate
⚠️ OPENAI 28 21 7 75.0%

❌ Failed Evaluations

Show 7 failures

OPENAI

1. TestReactEval/[openai]_react_cat_message

  • Score: 0.00
  • Rubric: The word/emoji is a cat emoji or a heart/love emoji
  • Reason: The output is the text string "heart_eyes_cat", not an actual cat emoji (e.g., 😺/🐱) or a heart/love emoji (e.g., ❤️/😍).

2. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: is a list of bugs
  • Reason: The output provides a disclaimer and a blank, ready-to-fill template rather than an actual list of bugs. It does not enumerate any specific bug entries.

3. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: includes a description of each bug
  • Reason: The output provides a blank template and does not actually include descriptions of any specific bugs. It asks the user to paste bug reports instead of describing each bug.

4. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes each bug to a user
  • Reason: The output provides an empty template and asks the user to paste bug reports; it does not actually list any bugs or attribute each bug to a specific user (reporter).

5. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes the bug about trying to save without a color and the save button not doing anything to @maria.nunez
  • Reason: The output does not mention the specific bug about trying to save without a color or that the save button does nothing, and it does not attribute any bug to @maria.nunez.

6. TestConversationMentionHandling/[openai]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: the bug about the end user being able to change channel banner is attributed to @maria.nunez
  • Reason: The output provides a generic template and does not mention any specific bug about end users being able to change the channel banner, nor does it attribute such a bug to @maria.nunez.

7. TestDirectMessageConversations/[openai]_bot_dm_tool_introspection

  • Score: 0.00
  • Rubric: mentions Github and refers to the documentation
  • Reason: The output refers to documentation via a Mattermost docs link, but it does not mention GitHub anywhere. Since the rubric requires both mentioning GitHub and referring to the documentation, it fails.

Anthropic

⚠️ Overall: 21/28 tests passed (75.0%)

Provider Total Passed Failed Pass Rate
⚠️ ANTHROPIC 28 21 7 75.0%

❌ Failed Evaluations

Show 7 failures

ANTHROPIC

1. TestReactEval/[anthropic]_react_cat_message

  • Score: 0.00
  • Rubric: The word/emoji is a cat emoji or a heart/love emoji
  • Reason: The output is the text string "heart_eyes_cat", not an actual cat emoji (e.g., 😺/🐱) or a heart/love emoji (e.g., ❤️/😍).

2. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: is a list of bugs
  • Reason: The output does not provide a list of bugs; it states the assistant cannot access channel history and suggests ways to compile a list, but no actual bug list is included.

3. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: includes a description of each bug
  • Reason: The output states it cannot access channel history and suggests ways to compile a list, but it does not describe any specific bugs. Therefore, it does not include a description of each bug.

4. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes each bug to a user
  • Reason: The output does not list any bugs, and it does not attribute any bug to any user. It only states it cannot access history and suggests ways to compile a list.

5. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: attributes the bug about trying to save without a color and the save button not doing anything to @maria.nunez
  • Reason: The output does not mention @maria.nunez or attribute any bug (including the one about saving without a color and the save button doing nothing) to them; it only states lack of access and suggests ways to search/compile bugs.

6. TestConversationMentionHandling/[anthropic]_conversation_from_attribution_long_thread.json

  • Score: 0.00
  • Rubric: the bug about the end user being able to change channel banner is attributed to @maria.nunez
  • Reason: The output explains it cannot access channel history and suggests ways to search for bugs, but it does not mention the specific bug about an end user being able to change the channel banner, nor does it attribute that bug to @maria.nunez.

7. TestDirectMessageConversations/[anthropic]_bot_dm_tool_introspection

  • Score: 0.00
  • Rubric: mentions Github and refers to the documentation
  • Reason: The output refers to the documentation (docs.mattermost.com) but does not mention GitHub anywhere, so it does not satisfy the rubric requirement to mention GitHub and refer to the documentation.

This comment was automatically generated by the eval CI pipeline.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds the deferred AskAnotherUser tool flow. It introduces waiting tool states, built-in tool policies, server-side response handling, interactive web cards, shared answer controls, documentation, and end-to-end coverage.

Changes

Ask Another User deferred workflow

Layer / File(s) Summary
Deferred tool state and persistence
llm/*, conversation/*, streaming/*, webapp/src/types/*, webapp/src/components/llmbot_post/*
Adds waiting and deferred_result support across tool models, conversions, persistence, approval state, retries, and client types.
Deferred dispatch and continuation
toolrunner/*, conversations/tool_approval.go, conversations/conversations.go, conversations/handle_messages.go, conversations/regeneration.go, mcp/*
Adds deferred dispatch, waiting-state persistence, unresolved-call gating, retry handling, built-in policy resolution, and conversation-scoped dispatchers.
Ask Another User tool and response service
mmtools/*, conversations/ask_another_user.go, api/api.go, api/api_post.go, config/mcp_config.go, i18n/en.json
Adds tool schemas, validation, question-card creation, response processing, result persistence, policy seeding, and an authenticated response endpoint.
Ask User web experience
webapp/src/client*, webapp/src/components/*, webapp/src/i18n/en.json, webapp/src/index.tsx
Adds response submission, reusable question-option controls, Ask User card rendering, waiting and declined tool states, translations, registration, and UI tests.
Documentation and end-to-end validation
docs/*, e2e/*
Documents built-in tool policies and the Ask Another User workflow. Adds serial end-to-end tests for answer and decline flows.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ToolRunner
  participant Conversations
  participant MattermostAPI
  participant AskUserPost
  participant TargetUser
  ToolRunner->>Conversations: Dispatch deferred AskAnotherUser call
  Conversations->>MattermostAPI: Create pending question card
  MattermostAPI->>AskUserPost: Deliver custom_llm_ask_user post
  AskUserPost->>TargetUser: Display question and answer controls
  TargetUser->>AskUserPost: Submit answer or decline
  AskUserPost->>MattermostAPI: POST ask_user_response
  MattermostAPI->>Conversations: Validate and persist response
  Conversations->>ToolRunner: Resume when unresolved tool calls are resolved
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding the AskAnotherUser tool with user-facing disclosure and cancellation controls.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/agent-ask-user-tool-5d96

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (9)
conversations/ask_another_user.go (1)

89-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the span-derived context for the rest of the function.

telemetry.Tracer().Start returns a context that carries the new span. The code discards it with _. Downstream calls in this function do not take a context today, so no trace is lost yet. A later ctx-aware call would attach to the parent span instead of this one.

♻️ Proposed change
-	_, span := telemetry.Tracer().Start(ctx, "dispatch ask another user",
+	ctx, span := telemetry.Tracer().Start(ctx, "dispatch ask another user",

ctx is then used by any context-aware call added later. If no call uses it now, keep _ = ctx out and add the assignment together with the first consumer.

As per coding guidelines: "Thread ctx context.Context as the first parameter through every entry point in the LLM call path, avoid context.Background() in production code, and add OpenTelemetry spans with the repo's telemetry helpers and attribute keys."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@conversations/ask_another_user.go` around lines 89 - 95, Update
dispatchAskAnotherUser to retain the context returned by
telemetry.Tracer().Start instead of discarding it, and use that span-derived
context for subsequent function operations and any context-aware calls. Preserve
the existing span attributes and avoid introducing a separate context or
context.Background().

Source: Coding guidelines

webapp/src/index.tsx (1)

34-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the exported AskUserPostType constant.

webapp/src/components/ask_user_post/ask_user_post.tsx line 23 exports AskUserPostType = 'custom_llm_ask_user' and documents it as the server contract. Importing the constant here removes the duplicated literal and prevents drift.

♻️ Proposed change
-import {AskUserPost} from './components/ask_user_post/ask_user_post';
+import {AskUserPost, AskUserPostType} from './components/ask_user_post/ask_user_post';
-        registry.registerPostTypeComponent('custom_llm_ask_user', AskUserPost);
+        registry.registerPostTypeComponent(AskUserPostType, AskUserPost);

Also applies to: 235-235

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/index.tsx` at line 34, Update the usage in webapp/src/index.tsx
associated with AskUserPost to import and use the exported AskUserPostType
constant from ask_user_post.tsx instead of duplicating the 'custom_llm_ask_user'
literal, preserving the existing server-contract behavior.
webapp/src/components/tool_card.test.tsx (1)

188-205: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a case for the unknown-decliner render branch.

webapp/src/components/tool_card.tsx lines 682-687 render Declined to answer when declinedBy is ''. The parse tests cover the '' return value, but no test renders that branch. Add a rejected AskAnotherUser tool whose result is {"status":"declined"} with no arguments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/tool_card.test.tsx` around lines 188 - 205, Add a test
to the “ToolCard declined rendering” suite that renders a rejected
AskAnotherUser tool with result {"status":"declined"} and no target username,
then assert the UI displays “Declined to answer”. Keep the existing
named-decliner and ordinary rejected-tool cases unchanged.
webapp/src/components/question_card.tsx (1)

218-234: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider moving the interactivity guard and the submit rule into the shared hook.

handleToggleOption, handleToggleFreeForm, and the canSubmit expression are identical to guardedToggleOption, guardedToggleFreeForm, and the options branch of canSubmit in webapp/src/components/ask_user_post/ask_user_post.tsx (lines 384-414). If useOptionSelection accepted an interactive flag and exposed canSubmit, both callers would drop this duplicated logic and stay in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/question_card.tsx` around lines 218 - 234, Move the
shared interactivity guards and submission eligibility into useOptionSelection
by accepting an interactive flag and exposing canSubmit. Update
question_card.tsx and ask_user_post.tsx to use the hook’s guardedToggleOption,
guardedToggleFreeForm, and canSubmit, removing their duplicated handlers and
local canSubmit logic while preserving the existing free-form and
predefined-option rules.
webapp/src/components/question_options.tsx (1)

318-333: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Free-form answer fields have no accessible name. Each free-form field uses a placeholder as its only name. Screen readers announce no field name, and the placeholder disappears once the user types. Add an aria-label with the same localized text at each site.

  • webapp/src/components/question_options.tsx#L318-L333: pass aria-label={freeFormPlaceholder} to both FreeFormTextarea and FreeFormInput.
  • webapp/src/components/ask_user_post/ask_user_post.tsx#L484-L495: pass aria-label with the ai.ask_user.free_form_placeholder message to the standalone FreeFormTextarea.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/question_options.tsx` around lines 318 - 333, Give both
free-form controls in webapp/src/components/question_options.tsx lines 318-333,
FreeFormTextarea and FreeFormInput, an aria-label using freeFormPlaceholder.
Also add an aria-label with the localized ai.ask_user.free_form_placeholder text
to the standalone FreeFormTextarea in
webapp/src/components/ask_user_post/ask_user_post.tsx lines 484-495.
mcp/vetted_tools.go (1)

79-87: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Avoid duplicating the built-in tool name.

mcp/vetted_tools.go hardcodes "AskAnotherUser" while the registered tool exposes mmtools.AskAnotherUserToolName. If either value changes, the policy seed will no longer match the tool. Use a shared dependency-neutral constant, or reuse the existing constant when the import graph permits. Add a test that checks the seed name against the registered tool name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp/vetted_tools.go` around lines 79 - 87, Update SeedBuiltInToolConfigs to
source the AskAnotherUser policy name from a shared dependency-neutral constant,
or reuse mmtools.AskAnotherUserToolName if the import graph allows, instead of
hardcoding the string. Add a test that verifies the seeded name matches the
registered AskAnotherUser tool name.
conversations/tool_approval.go (1)

402-409: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse persistBlocks for the remaining inline marshal-and-update.

The new helper duplicates the marshal-then-UpdateTurnContent sequence that still appears inline later in HandleToolCall. Routing that call through persistBlocks keeps one error-wrapping path for turn writes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@conversations/tool_approval.go` around lines 402 - 409, Update the inline
marshal-and-UpdateTurnContent sequence in HandleToolCall to call persistBlocks
instead, passing the same turn ID and blocks. Remove the duplicated JSON
marshaling and preserve persistBlocks’s existing error-wrapping behavior.
toolrunner/toolrunner_deferred_test.go (1)

46-246: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider a table for the deferred dispatch cases.

TestRunLoopDeferredDispatch contains six cases as separate t.Run closures. The repository guideline requires table-driven tests when a test contains more than one case. The cases differ in the scripted LLM responses, the tool store, and the approval function, so a table would need those as fields. conversations/ask_another_user_test.go in this same change uses that shape already.

As per coding guidelines: "Go tests must be table-driven when they contain more than one case."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@toolrunner/toolrunner_deferred_test.go` around lines 46 - 246, Refactor
TestRunLoopDeferredDispatch into a table-driven test with one case entry per
deferred-dispatch scenario, including fields for scripted responses, tool
configuration, approval function, dispatcher error, and case-specific
assertions. Iterate with t.Run using the existing testLLM, dispatchRecorder, and
collectToolCallEvents helpers, preserving each case’s expected statuses, call
counts, tool turns, and resolver behavior.

Source: Coding guidelines

mcp/tool_policy_lookup_test.go (1)

226-284: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Convert these five cases to a table.

All five subtests share one shape: a Config, a tool name, an expected policy, and an expected enabled flag. A table with those four fields covers them without losing the explanatory comment on the WebSearch case.

As per coding guidelines: "Go tests must be table-driven when they contain more than one case."

♻️ Proposed table shape
func TestLookupToolPolicyBuiltIn(t *testing.T) {
	const builtInToolName = "AskAnotherUser"

	tests := []struct {
		name        string
		cfg         Config
		toolName    string
		wantPolicy  string
		wantEnabled bool
	}{
		{
			name:        "unconfigured AskAnotherUser gets the seed",
			toolName:    builtInToolName,
			wantPolicy:  ToolPolicyAsk,
			wantEnabled: true,
		},
		{
			name: "admin auto_run_in_dm override wins over the seed",
			cfg: Config{BuiltInTools: []ToolConfig{{
				Name: builtInToolName, Policy: ToolPolicyAutoRunInDM, Enabled: true,
			}}},
			toolName:    builtInToolName,
			wantPolicy:  ToolPolicyAutoRunInDM,
			wantEnabled: true,
		},
		{
			name: "admin disable wins over the seed",
			cfg: Config{BuiltInTools: []ToolConfig{{
				Name: builtInToolName, Policy: ToolPolicyAsk, Enabled: false,
			}}},
			toolName:   builtInToolName,
			wantPolicy: ToolPolicyAsk,
		},
		{
			// ask+enabled still cannot auto-run: enabled only gates auto-run.
			name:        "unconfigured WebSearch stays at ask",
			toolName:    "WebSearch",
			wantPolicy:  ToolPolicyAsk,
			wantEnabled: true,
		},
		{
			name:       "empty tool name stays closed",
			wantPolicy: ToolPolicyAsk,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			policy, enabled := LookupToolPolicy(tt.cfg, "", tt.toolName)

			require.Equal(t, tt.wantPolicy, policy)
			require.Equal(t, tt.wantEnabled, enabled)
			require.False(t, IsToolPolicyAutoRunInDM(policy) && !tt.wantEnabled)
		})
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp/tool_policy_lookup_test.go` around lines 226 - 284, Convert
TestLookupToolPolicyBuiltIn into a table-driven test with fields for the case
name, Config, tool name, expected policy, and expected enabled state. Iterate
through the cases with t.Run, preserving the WebSearch explanatory comment and
its auto-run assertion while retaining each existing expected result.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@conversations/ask_another_user.go`:
- Around line 258-266: The waiting-state transition in the flow around
findToolUseBlock must be atomic: replace the unconditional
persistBlocks/UpdateTurnContent operation with a conditional store update that
modifies the block only when its status is still conversation.StatusWaiting.
Check the affected-row count and return ErrAskNotPending when another request
has already transitioned it, before creating the tool_result turn or starting
the follow-up stream.
- Around line 398-404: The asynchronous follow-up path in HandleAskUserResponse
must detach the request context before invoking streamToolFollowUp. Apply
telemetry.DetachContext at this API boundary and pass the detached context
through the existing follow-up flow so streamContinuationToExistingPost is not
cancelled when the handler returns.
- Around line 129-131: Update AskAnotherUser around dispatchAskAnotherUser to
check source-channel permissions before sending the DM card: when conv.ChannelID
identifies a non-open channel, require the target user to have
model.PermissionReadChannel and return an access error if not. Preserve the
existing usage-restriction check and behavior for open channels.

In `@conversations/tool_approval.go`:
- Around line 184-210: Make approval handling in findPendingToolTurn and
UpdateTurnContent atomic so only one request can claim a pending block per
conversation before dispatchAskAnotherUser, using the existing locking or
transactional mechanism where available. Ensure persistBlocks and
CreateTurnAutoSequence commit the terminal block status and matching tool_result
together, or add recovery that reconciles any partial failure so no unresolved
persisted state remains.

In `@mmtools/ask_another_user.go`:
- Around line 90-92: Update the too-many-options error message in the options
validation near args.Options to match the enforced range: zero through five
options, while leaving the existing len(args.Options) > 5 check unchanged.
Ensure the tool error no longer claims a minimum of one option or conflicts with
the accepted behavior.

In `@toolrunner/toolrunner.go`:
- Around line 349-396: The mixed deferred/non-deferred batch path currently
discards failedResults, leaving failed deferred tool calls without persisted
tool_result entries. Update the handling around deliverToolTurns and the
resume/finalization flow to retain and persist each failed deferred call’s
ToolResult alongside the batch, while preserving pending non-deferred calls and
waiting deferred calls for resume; ensure StatusError calls are not skipped when
constructing the follow-up request.

In `@webapp/src/components/ask_user_post/ask_user_post.tsx`:
- Around line 135-137: Update buildAnswerPreview to join selected labels and
trimmed free-form text with " — " instead of ", ", while preserving empty-value
filtering. Replace the UTF-16 slice(0, 200) truncation with Unicode
code-point-aware truncation so the preview is limited to 200 runes, matching the
server rule.

In `@webapp/src/components/question_options.tsx`:
- Around line 278-284: Set the native disabled attribute on each button rendered
by OptionRow and FreeFormToggle when interactive is false, including the option
row, FreeFormToggle, and free-form OptionRow instances. Keep the existing
$disabled styling prop and click handlers unchanged.
- Around line 275-298: Update the options rendering in the options map to use an
index-qualified React key instead of key={opt.label}, and make selection state
row-specific so duplicate labels do not cause both rows to appear selected.
Apply the corresponding toggle/state handling consistently with the existing
onToggleOption flow; alternatively, if duplicate labels are intended to be
invalid, add duplicate-label validation to both parseQuestionArgs and
parseAskUserProps.

---

Nitpick comments:
In `@conversations/ask_another_user.go`:
- Around line 89-95: Update dispatchAskAnotherUser to retain the context
returned by telemetry.Tracer().Start instead of discarding it, and use that
span-derived context for subsequent function operations and any context-aware
calls. Preserve the existing span attributes and avoid introducing a separate
context or context.Background().

In `@conversations/tool_approval.go`:
- Around line 402-409: Update the inline marshal-and-UpdateTurnContent sequence
in HandleToolCall to call persistBlocks instead, passing the same turn ID and
blocks. Remove the duplicated JSON marshaling and preserve persistBlocks’s
existing error-wrapping behavior.

In `@mcp/tool_policy_lookup_test.go`:
- Around line 226-284: Convert TestLookupToolPolicyBuiltIn into a table-driven
test with fields for the case name, Config, tool name, expected policy, and
expected enabled state. Iterate through the cases with t.Run, preserving the
WebSearch explanatory comment and its auto-run assertion while retaining each
existing expected result.

In `@mcp/vetted_tools.go`:
- Around line 79-87: Update SeedBuiltInToolConfigs to source the AskAnotherUser
policy name from a shared dependency-neutral constant, or reuse
mmtools.AskAnotherUserToolName if the import graph allows, instead of hardcoding
the string. Add a test that verifies the seeded name matches the registered
AskAnotherUser tool name.

In `@toolrunner/toolrunner_deferred_test.go`:
- Around line 46-246: Refactor TestRunLoopDeferredDispatch into a table-driven
test with one case entry per deferred-dispatch scenario, including fields for
scripted responses, tool configuration, approval function, dispatcher error, and
case-specific assertions. Iterate with t.Run using the existing testLLM,
dispatchRecorder, and collectToolCallEvents helpers, preserving each case’s
expected statuses, call counts, tool turns, and resolver behavior.

In `@webapp/src/components/question_card.tsx`:
- Around line 218-234: Move the shared interactivity guards and submission
eligibility into useOptionSelection by accepting an interactive flag and
exposing canSubmit. Update question_card.tsx and ask_user_post.tsx to use the
hook’s guardedToggleOption, guardedToggleFreeForm, and canSubmit, removing their
duplicated handlers and local canSubmit logic while preserving the existing
free-form and predefined-option rules.

In `@webapp/src/components/question_options.tsx`:
- Around line 318-333: Give both free-form controls in
webapp/src/components/question_options.tsx lines 318-333, FreeFormTextarea and
FreeFormInput, an aria-label using freeFormPlaceholder. Also add an aria-label
with the localized ai.ask_user.free_form_placeholder text to the standalone
FreeFormTextarea in webapp/src/components/ask_user_post/ask_user_post.tsx lines
484-495.

In `@webapp/src/components/tool_card.test.tsx`:
- Around line 188-205: Add a test to the “ToolCard declined rendering” suite
that renders a rejected AskAnotherUser tool with result {"status":"declined"}
and no target username, then assert the UI displays “Declined to answer”. Keep
the existing named-decliner and ordinary rejected-tool cases unchanged.

In `@webapp/src/index.tsx`:
- Line 34: Update the usage in webapp/src/index.tsx associated with AskUserPost
to import and use the exported AskUserPostType constant from ask_user_post.tsx
instead of duplicating the 'custom_llm_ask_user' literal, preserving the
existing server-contract behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: dd7512ce-958b-4db8-bf69-2a633b5099fe

📥 Commits

Reviewing files that changed from the base of the PR and between 4604394 and 5b2389f.

📒 Files selected for processing (47)
  • api/api.go
  • api/api_post.go
  • api/api_post_test.go
  • config/mcp_config.go
  • conversation/approval_state.go
  • conversation/approval_state_test.go
  • conversation/content_block.go
  • conversation/convert.go
  • conversation/convert_test.go
  • conversation/service_test.go
  • conversations/ask_another_user.go
  • conversations/ask_another_user_test.go
  • conversations/bot_channel_tool_filter.go
  • conversations/conversations.go
  • conversations/handle_messages.go
  • conversations/regeneration.go
  • conversations/tool_approval.go
  • conversations/tool_policy_test.go
  • i18n/en.json
  • llm/tool_retry.go
  • llm/tool_retry_test.go
  • llm/tools.go
  • mcp/tool_policy.go
  • mcp/tool_policy_lookup_test.go
  • mcp/vetted_tools.go
  • mcp/vetted_tools_test.go
  • mmtools/ask_another_user.go
  • mmtools/ask_another_user_test.go
  • mmtools/provider.go
  • streaming/streaming.go
  • streaming/turn_persistence_test.go
  • toolrunner/toolrunner.go
  • toolrunner/toolrunner_deferred_test.go
  • webapp/src/client.test.ts
  • webapp/src/client.tsx
  • webapp/src/components/ask_user_post/ask_user_post.test.tsx
  • webapp/src/components/ask_user_post/ask_user_post.tsx
  • webapp/src/components/llmbot_post/turn_content_utils.test.ts
  • webapp/src/components/llmbot_post/turn_content_utils.ts
  • webapp/src/components/question_card.tsx
  • webapp/src/components/question_options.tsx
  • webapp/src/components/tool_card.test.tsx
  • webapp/src/components/tool_card.tsx
  • webapp/src/components/tool_types.ts
  • webapp/src/i18n/en.json
  • webapp/src/index.tsx
  • webapp/src/types/conversation.ts

Comment thread conversations/ask_another_user.go
Comment thread conversations/ask_another_user.go
Comment thread conversations/ask_another_user.go
Comment thread conversations/tool_approval.go
Comment thread mmtools/ask_another_user.go
Comment thread toolrunner/toolrunner.go
Comment thread webapp/src/components/ask_user_post/ask_user_post.tsx Outdated
Comment thread webapp/src/components/question_options.tsx
Comment thread webapp/src/components/question_options.tsx
cursoragent and others added 4 commits August 3, 2026 06:27
… param, preview parity

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
…arg lengths

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
conversations/ask_another_user_test.go (1)

218-232: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Cover requester lookup failures in the attribution test.

The dispatcher leaves the plaintext fallback without attribution when GetUser(conv.UserID) fails. This table covers a human requester and an autonomous bot requester, but not a lookup failure. Add a case that verifies the intended behaviour, or make dispatch fail before sending when attribution is required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@conversations/ask_another_user_test.go` around lines 218 - 232, Extend the
table-driven attribution test around the existing wantRequesterProp assertions
to cover a GetUser(conv.UserID) lookup failure, verifying the intended
no-attribution plaintext fallback behavior (or the required pre-send dispatch
failure). Configure the case using the test’s existing user-lookup/mock symbols
and assert the corresponding sent message or error outcome.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mmtools/ask_another_user.go`:
- Around line 90-96: Canonicalize args.Username in ValidateAskAnotherUserArgs by
trimming surrounding whitespace and removing the optional leading @, then reject
the canonical value when empty. Reuse that same canonical username in
conversations.dispatchAskAnotherUser when calling GetUserByUsername, rather than
passing the original input.

---

Nitpick comments:
In `@conversations/ask_another_user_test.go`:
- Around line 218-232: Extend the table-driven attribution test around the
existing wantRequesterProp assertions to cover a GetUser(conv.UserID) lookup
failure, verifying the intended no-attribution plaintext fallback behavior (or
the required pre-send dispatch failure). Configure the case using the test’s
existing user-lookup/mock symbols and assert the corresponding sent message or
error outcome.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 005c3897-69f4-4117-b473-8ef5568bdfea

📥 Commits

Reviewing files that changed from the base of the PR and between c237ddf and 67aefc1.

📒 Files selected for processing (5)
  • conversations/ask_another_user.go
  • conversations/ask_another_user_test.go
  • i18n/en.json
  • mmtools/ask_another_user.go
  • mmtools/ask_another_user_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • mmtools/ask_another_user_test.go
  • conversations/ask_another_user.go

Comment thread mmtools/ask_another_user.go
@crspeller
crspeller marked this pull request as ready for review August 4, 2026 16:12

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67aefc1525

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread toolrunner/toolrunner.go
Comment thread conversations/ask_another_user.go Outdated
Comment thread conversations/tool_approval.go
cursoragent and others added 2 commits August 4, 2026 18:23
… username

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
…ype constant)

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
Co-authored-by: christopher <christopher@mattermost.com>
cursoragent and others added 10 commits August 7, 2026 16:50
…nflicts

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
…ops, sanitize, cancel endpoint

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
…t separation), experimental toggle, cancel control

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
…laim, cancel button hydration, identity-line alignment

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
…er-tool-5d96

Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
Co-authored-by: Christopher Speller <crspeller@users.noreply.github.com>
@cursor cursor Bot changed the title Add AskAnotherUser tool: agent can ask another user a clarifying question Add experimental AskAnotherUser tool with disclosure and cancel controls Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants