[elicitation] User-friendly tool call messages. (#14403)
- [x] Add a curated set of tool call messages and human-readable tool param names.
This commit is contained in:
parent
19d0949aab
commit
23e55d7668
6 changed files with 1782 additions and 81 deletions
962
codex-rs/core/src/consequential_tool_message_templates.json
Normal file
962
codex-rs/core/src/consequential_tool_message_templates.json
Normal file
|
|
@ -0,0 +1,962 @@
|
|||
{
|
||||
"schema_version": 4,
|
||||
"templates": [
|
||||
{
|
||||
"source_tool_index": 0,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "add_comment_to_issue",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "pr_number",
|
||||
"label": "Pull request"
|
||||
},
|
||||
{
|
||||
"name": "repo_full_name",
|
||||
"label": "Repository"
|
||||
},
|
||||
{
|
||||
"name": "comment",
|
||||
"label": "Comment"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to add a comment to a pull request?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 1,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "add_reaction_to_issue_comment",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "reaction",
|
||||
"label": "Reaction"
|
||||
},
|
||||
{
|
||||
"name": "comment_id",
|
||||
"label": "Comment"
|
||||
},
|
||||
{
|
||||
"name": "repo_full_name",
|
||||
"label": "Repository"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to add a reaction to an issue comment?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 2,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "add_reaction_to_pr",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "reaction",
|
||||
"label": "Reaction"
|
||||
},
|
||||
{
|
||||
"name": "pr_number",
|
||||
"label": "Pull request"
|
||||
},
|
||||
{
|
||||
"name": "repo_full_name",
|
||||
"label": "Repository"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to add a reaction to a pull request?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 3,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "add_reaction_to_pr_review_comment",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "reaction",
|
||||
"label": "Reaction"
|
||||
},
|
||||
{
|
||||
"name": "comment_id",
|
||||
"label": "Comment"
|
||||
},
|
||||
{
|
||||
"name": "repo_full_name",
|
||||
"label": "Repository"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to add a reaction to a pull request review comment?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 4,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "add_review_to_pr",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "action",
|
||||
"label": "Action"
|
||||
},
|
||||
{
|
||||
"name": "pr_number",
|
||||
"label": "Pull request"
|
||||
},
|
||||
{
|
||||
"name": "review",
|
||||
"label": "Review"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to submit a pull request review?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 5,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_blob",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "repository_full_name",
|
||||
"label": "Repository"
|
||||
},
|
||||
{
|
||||
"name": "content",
|
||||
"label": "Content"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a Git blob?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 6,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_branch",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "branch_name",
|
||||
"label": "Branch"
|
||||
},
|
||||
{
|
||||
"name": "repository_full_name",
|
||||
"label": "Repository"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a branch?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 7,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_commit",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "repository_full_name",
|
||||
"label": "Repository"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"label": "Message"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a commit?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 8,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_pull_request",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "title",
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"name": "head_branch",
|
||||
"label": "Head branch"
|
||||
},
|
||||
{
|
||||
"name": "base_branch",
|
||||
"label": "Base branch"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a pull request?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 9,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_tree",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "repository_full_name",
|
||||
"label": "Repository"
|
||||
},
|
||||
{
|
||||
"name": "tree_elements",
|
||||
"label": "Changes"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a Git tree?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 10,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "enable_auto_merge",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "pr_number",
|
||||
"label": "Pull request"
|
||||
},
|
||||
{
|
||||
"name": "repository_full_name",
|
||||
"label": "Repository"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to enable pull request auto-merge?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 11,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "label_pr",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "label",
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"name": "pr_number",
|
||||
"label": "Pull request"
|
||||
},
|
||||
{
|
||||
"name": "repository_full_name",
|
||||
"label": "Repository"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to add a label to a pull request?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 12,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "remove_reaction_from_issue_comment",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "reaction_id",
|
||||
"label": "Reaction"
|
||||
},
|
||||
{
|
||||
"name": "comment_id",
|
||||
"label": "Comment"
|
||||
},
|
||||
{
|
||||
"name": "repo_full_name",
|
||||
"label": "Repository"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to remove a reaction from an issue comment?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 13,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "remove_reaction_from_pr",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "reaction_id",
|
||||
"label": "Reaction"
|
||||
},
|
||||
{
|
||||
"name": "pr_number",
|
||||
"label": "Pull request"
|
||||
},
|
||||
{
|
||||
"name": "repo_full_name",
|
||||
"label": "Repository"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to remove a reaction from a pull request?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 14,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "remove_reaction_from_pr_review_comment",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "reaction_id",
|
||||
"label": "Reaction"
|
||||
},
|
||||
{
|
||||
"name": "comment_id",
|
||||
"label": "Comment"
|
||||
},
|
||||
{
|
||||
"name": "repo_full_name",
|
||||
"label": "Repository"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to remove a reaction from a pull request review comment?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 15,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "reply_to_review_comment",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "pr_number",
|
||||
"label": "Pull request"
|
||||
},
|
||||
{
|
||||
"name": "repo_full_name",
|
||||
"label": "Repository"
|
||||
},
|
||||
{
|
||||
"name": "comment",
|
||||
"label": "Comment"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to reply to a pull request review comment?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 16,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "update_issue_comment",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "comment_id",
|
||||
"label": "Comment ID"
|
||||
},
|
||||
{
|
||||
"name": "repo_full_name",
|
||||
"label": "Repository"
|
||||
},
|
||||
{
|
||||
"name": "comment",
|
||||
"label": "Comment"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to update an issue comment?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 17,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "update_ref",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "branch_name",
|
||||
"label": "Branch"
|
||||
},
|
||||
{
|
||||
"name": "repository_full_name",
|
||||
"label": "Repository"
|
||||
},
|
||||
{
|
||||
"name": "sha",
|
||||
"label": "Commit"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to update a branch reference?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 18,
|
||||
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "update_review_comment",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "comment_id",
|
||||
"label": "Comment ID"
|
||||
},
|
||||
{
|
||||
"name": "repo_full_name",
|
||||
"label": "Repository"
|
||||
},
|
||||
{
|
||||
"name": "comment",
|
||||
"label": "Comment"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to update a pull request review comment?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 19,
|
||||
"connector_id": "connector_947e0d954944416db111db556030eea6",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_event",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "title",
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"name": "start_time",
|
||||
"label": "Start"
|
||||
},
|
||||
{
|
||||
"name": "attendees",
|
||||
"label": "Attendees"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create an event?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 20,
|
||||
"connector_id": "connector_947e0d954944416db111db556030eea6",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "delete_event",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "event_id",
|
||||
"label": "Event"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to delete an event?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 21,
|
||||
"connector_id": "connector_947e0d954944416db111db556030eea6",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "respond_event",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "response_status",
|
||||
"label": "Response Status"
|
||||
},
|
||||
{
|
||||
"name": "event_id",
|
||||
"label": "Event"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to respond to an event?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 22,
|
||||
"connector_id": "connector_947e0d954944416db111db556030eea6",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "update_event",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "event_id",
|
||||
"label": "Event"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to update an event?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 23,
|
||||
"connector_id": "connector_9d7cfa34e6654a5f98d3387af34b2e1c",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "batch_update",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "spreadsheet_url",
|
||||
"label": "Spreadsheet"
|
||||
},
|
||||
{
|
||||
"name": "requests",
|
||||
"label": "Changes"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to apply spreadsheet updates?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 24,
|
||||
"connector_id": "connector_9d7cfa34e6654a5f98d3387af34b2e1c",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_spreadsheet",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "title",
|
||||
"label": "Title"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a spreadsheet?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 25,
|
||||
"connector_id": "connector_9d7cfa34e6654a5f98d3387af34b2e1c",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "duplicate_sheet_in_new_file",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "source_sheet_name",
|
||||
"label": "Source Sheet Name"
|
||||
},
|
||||
{
|
||||
"name": "spreadsheet_url",
|
||||
"label": "Spreadsheet"
|
||||
},
|
||||
{
|
||||
"name": "new_file_name",
|
||||
"label": "New File Name"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to copy a sheet into a new spreadsheet?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 26,
|
||||
"connector_id": "connector_6f1ec045b8fa4ced8738e32c7f74514b",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "batch_update",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "presentation_url",
|
||||
"label": "Presentation"
|
||||
},
|
||||
{
|
||||
"name": "requests",
|
||||
"label": "Changes"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to apply presentation updates?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 27,
|
||||
"connector_id": "connector_6f1ec045b8fa4ced8738e32c7f74514b",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_presentation",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "title",
|
||||
"label": "Title"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a presentation?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 28,
|
||||
"connector_id": "connector_4964e3b22e3e427e9b4ae1acf2c1fa34",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "batch_update",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "document_url",
|
||||
"label": "Document"
|
||||
},
|
||||
{
|
||||
"name": "requests",
|
||||
"label": "Changes"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to apply document updates?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 29,
|
||||
"connector_id": "connector_4964e3b22e3e427e9b4ae1acf2c1fa34",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_document",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "title",
|
||||
"label": "Title"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a document?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 30,
|
||||
"connector_id": "connector_5f3c8c41a1e54ad7a76272c89e2554fa",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "copy_document",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "url",
|
||||
"label": "URL"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to copy a file?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 31,
|
||||
"connector_id": "connector_5f3c8c41a1e54ad7a76272c89e2554fa",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "share_document",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "url",
|
||||
"label": "URL"
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"label": "Permission"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to change file sharing?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 32,
|
||||
"connector_id": "asdk_app_69a1d78e929881919bba0dbda1f6436d",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "slack_send_message",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "channel_id",
|
||||
"label": "Conversation"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"label": "Message"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to send a message?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 33,
|
||||
"connector_id": "asdk_app_69a1d78e929881919bba0dbda1f6436d",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "slack_schedule_message",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "channel_id",
|
||||
"label": "Conversation"
|
||||
},
|
||||
{
|
||||
"name": "post_at",
|
||||
"label": "Send at"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"label": "Message"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to schedule a message?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 34,
|
||||
"connector_id": "asdk_app_69a1d78e929881919bba0dbda1f6436d",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "slack_create_canvas",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "title",
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"name": "content",
|
||||
"label": "Content"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a canvas?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 35,
|
||||
"connector_id": "asdk_app_69a1d78e929881919bba0dbda1f6436d",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "slack_send_message_draft",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "channel_id",
|
||||
"label": "Conversation"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"label": "Message"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a message draft?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 36,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "add_comment_to_issue",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "issue_id",
|
||||
"label": "Issue"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"label": "Body"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to add a comment to an issue?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 37,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "add_label_to_issue",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "label_id",
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"name": "issue_id",
|
||||
"label": "Issue"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to add a label to an issue?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 38,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "add_url_attachment_to_issue",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "url",
|
||||
"label": "URL"
|
||||
},
|
||||
{
|
||||
"name": "issue_id",
|
||||
"label": "Issue"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"label": "Title"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to attach a link to an issue?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 39,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "assign_issue",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "issue_id",
|
||||
"label": "Issue"
|
||||
},
|
||||
{
|
||||
"name": "user_id",
|
||||
"label": "User"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to assign an issue?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 40,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_issue",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "title",
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"name": "team_id",
|
||||
"label": "Team"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create an issue?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 41,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_label",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "label_name",
|
||||
"label": "Label Name"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a label?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 42,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_project",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "name",
|
||||
"label": "Name"
|
||||
},
|
||||
{
|
||||
"name": "team_id",
|
||||
"label": "Team"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a project?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 43,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "remove_label_from_issue",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "label_id",
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"name": "issue_id",
|
||||
"label": "Issue"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to remove a label from an issue?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 44,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "resolve_comment",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "comment_id",
|
||||
"label": "Comment"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to resolve a comment?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 45,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "set_issue_state",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "issue_id",
|
||||
"label": "Issue"
|
||||
},
|
||||
{
|
||||
"name": "state_id",
|
||||
"label": "State"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to change issue state?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 46,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "unassign_issue",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "issue_id",
|
||||
"label": "Issue"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to unassign an issue?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 47,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "update_issue",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "issue_id",
|
||||
"label": "Issue"
|
||||
},
|
||||
{
|
||||
"name": "issue_update",
|
||||
"label": "Changes"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to update an issue?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 48,
|
||||
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "update_project",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "project_id",
|
||||
"label": "Project"
|
||||
},
|
||||
{
|
||||
"name": "update_fields",
|
||||
"label": "Changes"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to update a project?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 49,
|
||||
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "apply_labels_to_emails",
|
||||
"template_params": [],
|
||||
"template": "Allow {connector_name} to apply label changes to messages?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 50,
|
||||
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "batch_modify_email",
|
||||
"template_params": [],
|
||||
"template": "Allow {connector_name} to update message labels?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 51,
|
||||
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "bulk_label_matching_emails",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "label_name",
|
||||
"label": "Label Name"
|
||||
},
|
||||
{
|
||||
"name": "query",
|
||||
"label": "Query"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to label matching messages?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 52,
|
||||
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_draft",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "to",
|
||||
"label": "To"
|
||||
},
|
||||
{
|
||||
"name": "subject",
|
||||
"label": "Subject"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"label": "Body"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create an email draft?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 53,
|
||||
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "create_label",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "name",
|
||||
"label": "Name"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to create a label?"
|
||||
},
|
||||
{
|
||||
"source_tool_index": 54,
|
||||
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
|
||||
"server_name": "codex_apps",
|
||||
"tool_title": "send_email",
|
||||
"template_params": [
|
||||
{
|
||||
"name": "to",
|
||||
"label": "To"
|
||||
},
|
||||
{
|
||||
"name": "subject",
|
||||
"label": "Subject"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"label": "Body"
|
||||
}
|
||||
],
|
||||
"template": "Allow {connector_name} to send an email?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -47,6 +47,7 @@ pub mod instructions;
|
|||
pub mod landlock;
|
||||
pub mod mcp;
|
||||
mod mcp_connection_manager;
|
||||
mod mcp_tool_approval_templates;
|
||||
pub mod models_manager;
|
||||
mod network_policy_decision;
|
||||
pub mod network_proxy_loader;
|
||||
|
|
|
|||
365
codex-rs/core/src/mcp_tool_approval_templates.rs
Normal file
365
codex-rs/core/src/mcp_tool_approval_templates.rs
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
use std::collections::HashSet;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
use tracing::warn;
|
||||
|
||||
const CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION: u8 = 4;
|
||||
const CONNECTOR_NAME_TEMPLATE_VAR: &str = "{connector_name}";
|
||||
|
||||
static CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES: LazyLock<
|
||||
Option<Vec<ConsequentialToolMessageTemplate>>,
|
||||
> = LazyLock::new(load_consequential_tool_message_templates);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct RenderedMcpToolApprovalTemplate {
|
||||
pub(crate) question: String,
|
||||
pub(crate) elicitation_message: String,
|
||||
pub(crate) tool_params: Option<Value>,
|
||||
pub(crate) tool_params_display: Vec<RenderedMcpToolApprovalParam>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
pub(crate) struct RenderedMcpToolApprovalParam {
|
||||
pub(crate) name: String,
|
||||
pub(crate) value: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ConsequentialToolMessageTemplatesFile {
|
||||
schema_version: u8,
|
||||
templates: Vec<ConsequentialToolMessageTemplate>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
struct ConsequentialToolMessageTemplate {
|
||||
connector_id: String,
|
||||
server_name: String,
|
||||
tool_title: String,
|
||||
template: String,
|
||||
template_params: Vec<ConsequentialToolTemplateParam>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
struct ConsequentialToolTemplateParam {
|
||||
name: String,
|
||||
label: String,
|
||||
}
|
||||
|
||||
pub(crate) fn render_mcp_tool_approval_template(
|
||||
server_name: &str,
|
||||
connector_id: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
tool_title: Option<&str>,
|
||||
tool_params: Option<&Value>,
|
||||
) -> Option<RenderedMcpToolApprovalTemplate> {
|
||||
let templates = CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES.as_ref()?;
|
||||
render_mcp_tool_approval_template_from_templates(
|
||||
templates,
|
||||
server_name,
|
||||
connector_id,
|
||||
connector_name,
|
||||
tool_title,
|
||||
tool_params,
|
||||
)
|
||||
}
|
||||
|
||||
fn load_consequential_tool_message_templates() -> Option<Vec<ConsequentialToolMessageTemplate>> {
|
||||
let templates = match serde_json::from_str::<ConsequentialToolMessageTemplatesFile>(
|
||||
include_str!("consequential_tool_message_templates.json"),
|
||||
) {
|
||||
Ok(templates) => templates,
|
||||
Err(err) => {
|
||||
warn!(error = %err, "failed to parse consequential tool approval templates");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if templates.schema_version != CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION {
|
||||
warn!(
|
||||
found_schema_version = templates.schema_version,
|
||||
expected_schema_version = CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION,
|
||||
"unexpected consequential tool approval templates schema version"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(templates.templates)
|
||||
}
|
||||
|
||||
fn render_mcp_tool_approval_template_from_templates(
|
||||
templates: &[ConsequentialToolMessageTemplate],
|
||||
server_name: &str,
|
||||
connector_id: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
tool_title: Option<&str>,
|
||||
tool_params: Option<&Value>,
|
||||
) -> Option<RenderedMcpToolApprovalTemplate> {
|
||||
let connector_id = connector_id?;
|
||||
let tool_title = tool_title.map(str::trim).filter(|name| !name.is_empty())?;
|
||||
let template = templates.iter().find(|template| {
|
||||
template.server_name == server_name
|
||||
&& template.connector_id == connector_id
|
||||
&& template.tool_title == tool_title
|
||||
})?;
|
||||
let elicitation_message = render_question_template(&template.template, connector_name)?;
|
||||
let (tool_params, tool_params_display) = match tool_params {
|
||||
Some(Value::Object(tool_params)) => {
|
||||
render_tool_params(tool_params, &template.template_params)?
|
||||
}
|
||||
Some(_) => return None,
|
||||
None => (None, Vec::new()),
|
||||
};
|
||||
|
||||
Some(RenderedMcpToolApprovalTemplate {
|
||||
question: elicitation_message.clone(),
|
||||
elicitation_message,
|
||||
tool_params,
|
||||
tool_params_display,
|
||||
})
|
||||
}
|
||||
|
||||
fn render_question_template(template: &str, connector_name: Option<&str>) -> Option<String> {
|
||||
let template = template.trim();
|
||||
if template.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if template.contains(CONNECTOR_NAME_TEMPLATE_VAR) {
|
||||
let connector_name = connector_name
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())?;
|
||||
return Some(template.replace(CONNECTOR_NAME_TEMPLATE_VAR, connector_name));
|
||||
}
|
||||
|
||||
Some(template.to_string())
|
||||
}
|
||||
|
||||
fn render_tool_params(
|
||||
tool_params: &Map<String, Value>,
|
||||
template_params: &[ConsequentialToolTemplateParam],
|
||||
) -> Option<(Option<Value>, Vec<RenderedMcpToolApprovalParam>)> {
|
||||
let mut relabeled = Map::new();
|
||||
let mut display_params = Vec::new();
|
||||
let mut handled_names = HashSet::new();
|
||||
|
||||
for template_param in template_params {
|
||||
let label = template_param.label.trim();
|
||||
if label.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let Some(value) = tool_params.get(&template_param.name) else {
|
||||
continue;
|
||||
};
|
||||
if relabeled.insert(label.to_string(), value.clone()).is_some() {
|
||||
return None;
|
||||
}
|
||||
display_params.push(RenderedMcpToolApprovalParam {
|
||||
name: label.to_string(),
|
||||
value: value.clone(),
|
||||
});
|
||||
handled_names.insert(template_param.name.as_str());
|
||||
}
|
||||
|
||||
let mut remaining_params = tool_params
|
||||
.iter()
|
||||
.filter(|(name, _)| !handled_names.contains(name.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
remaining_params.sort_by(|(left_name, _), (right_name, _)| left_name.cmp(right_name));
|
||||
|
||||
for (name, value) in remaining_params {
|
||||
if handled_names.contains(name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if relabeled.insert(name.clone(), value.clone()).is_some() {
|
||||
return None;
|
||||
}
|
||||
display_params.push(RenderedMcpToolApprovalParam {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Some((Some(Value::Object(relabeled)), display_params))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn renders_exact_match_with_readable_param_labels() {
|
||||
let templates = vec![ConsequentialToolMessageTemplate {
|
||||
connector_id: "calendar".to_string(),
|
||||
server_name: "codex_apps".to_string(),
|
||||
tool_title: "create_event".to_string(),
|
||||
template: "Allow {connector_name} to create an event?".to_string(),
|
||||
template_params: vec![
|
||||
ConsequentialToolTemplateParam {
|
||||
name: "calendar_id".to_string(),
|
||||
label: "Calendar".to_string(),
|
||||
},
|
||||
ConsequentialToolTemplateParam {
|
||||
name: "title".to_string(),
|
||||
label: "Title".to_string(),
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
let rendered = render_mcp_tool_approval_template_from_templates(
|
||||
&templates,
|
||||
"codex_apps",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
Some("create_event"),
|
||||
Some(&json!({
|
||||
"title": "Roadmap review",
|
||||
"calendar_id": "primary",
|
||||
"timezone": "UTC",
|
||||
})),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
Some(RenderedMcpToolApprovalTemplate {
|
||||
question: "Allow Calendar to create an event?".to_string(),
|
||||
elicitation_message: "Allow Calendar to create an event?".to_string(),
|
||||
tool_params: Some(json!({
|
||||
"Calendar": "primary",
|
||||
"Title": "Roadmap review",
|
||||
"timezone": "UTC",
|
||||
})),
|
||||
tool_params_display: vec![
|
||||
RenderedMcpToolApprovalParam {
|
||||
name: "Calendar".to_string(),
|
||||
value: json!("primary"),
|
||||
},
|
||||
RenderedMcpToolApprovalParam {
|
||||
name: "Title".to_string(),
|
||||
value: json!("Roadmap review"),
|
||||
},
|
||||
RenderedMcpToolApprovalParam {
|
||||
name: "timezone".to_string(),
|
||||
value: json!("UTC"),
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_when_no_exact_match_exists() {
|
||||
let templates = vec![ConsequentialToolMessageTemplate {
|
||||
connector_id: "calendar".to_string(),
|
||||
server_name: "codex_apps".to_string(),
|
||||
tool_title: "create_event".to_string(),
|
||||
template: "Allow {connector_name} to create an event?".to_string(),
|
||||
template_params: Vec::new(),
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
render_mcp_tool_approval_template_from_templates(
|
||||
&templates,
|
||||
"codex_apps",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
Some("delete_event"),
|
||||
Some(&json!({})),
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_when_relabeling_would_collide() {
|
||||
let templates = vec![ConsequentialToolMessageTemplate {
|
||||
connector_id: "calendar".to_string(),
|
||||
server_name: "codex_apps".to_string(),
|
||||
tool_title: "create_event".to_string(),
|
||||
template: "Allow {connector_name} to create an event?".to_string(),
|
||||
template_params: vec![ConsequentialToolTemplateParam {
|
||||
name: "calendar_id".to_string(),
|
||||
label: "timezone".to_string(),
|
||||
}],
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
render_mcp_tool_approval_template_from_templates(
|
||||
&templates,
|
||||
"codex_apps",
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
Some("create_event"),
|
||||
Some(&json!({
|
||||
"calendar_id": "primary",
|
||||
"timezone": "UTC",
|
||||
})),
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_templates_load() {
|
||||
assert_eq!(CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES.is_some(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_literal_template_without_connector_substitution() {
|
||||
let templates = vec![ConsequentialToolMessageTemplate {
|
||||
connector_id: "github".to_string(),
|
||||
server_name: "codex_apps".to_string(),
|
||||
tool_title: "add_comment".to_string(),
|
||||
template: "Allow GitHub to add a comment to a pull request?".to_string(),
|
||||
template_params: Vec::new(),
|
||||
}];
|
||||
|
||||
let rendered = render_mcp_tool_approval_template_from_templates(
|
||||
&templates,
|
||||
"codex_apps",
|
||||
Some("github"),
|
||||
None,
|
||||
Some("add_comment"),
|
||||
Some(&json!({})),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
Some(RenderedMcpToolApprovalTemplate {
|
||||
question: "Allow GitHub to add a comment to a pull request?".to_string(),
|
||||
elicitation_message: "Allow GitHub to add a comment to a pull request?".to_string(),
|
||||
tool_params: Some(json!({})),
|
||||
tool_params_display: Vec::new(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_when_connector_placeholder_has_no_value() {
|
||||
let templates = vec![ConsequentialToolMessageTemplate {
|
||||
connector_id: "calendar".to_string(),
|
||||
server_name: "codex_apps".to_string(),
|
||||
tool_title: "create_event".to_string(),
|
||||
template: "Allow {connector_name} to create an event?".to_string(),
|
||||
template_params: Vec::new(),
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
render_mcp_tool_approval_template_from_templates(
|
||||
&templates,
|
||||
"codex_apps",
|
||||
Some("calendar"),
|
||||
None,
|
||||
Some("create_event"),
|
||||
Some(&json!({})),
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,8 @@ use crate::guardian::guardian_approval_request_to_json;
|
|||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam;
|
||||
use crate::mcp_tool_approval_templates::render_mcp_tool_approval_template;
|
||||
use crate::protocol::EventMsg;
|
||||
use crate::protocol::McpInvocation;
|
||||
use crate::protocol::McpToolCallBeginEvent;
|
||||
|
|
@ -385,6 +387,16 @@ struct McpToolApprovalPromptOptions {
|
|||
allow_persistent_approval: bool,
|
||||
}
|
||||
|
||||
struct McpToolApprovalElicitationRequest<'a> {
|
||||
server: &'a str,
|
||||
metadata: Option<&'a McpToolApprovalMetadata>,
|
||||
tool_params: Option<&'a serde_json::Value>,
|
||||
tool_params_display: Option<&'a [RenderedMcpToolApprovalParam]>,
|
||||
question: RequestUserInputQuestion,
|
||||
message_override: Option<&'a str>,
|
||||
prompt_options: McpToolApprovalPromptOptions,
|
||||
}
|
||||
|
||||
const MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX: &str = "mcp_tool_call_approval";
|
||||
const MCP_TOOL_APPROVAL_ACCEPT: &str = "Allow";
|
||||
const MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION: &str = "Allow for this session";
|
||||
|
|
@ -403,6 +415,7 @@ const MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY: &str = "connector_description
|
|||
const MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: &str = "tool_title";
|
||||
const MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY: &str = "tool_description";
|
||||
const MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY: &str = "tool_params";
|
||||
const MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY: &str = "tool_params_display";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
struct McpToolApprovalKey {
|
||||
|
|
@ -503,14 +516,26 @@ async fn maybe_request_mcp_tool_approval(
|
|||
tool_call_mcp_elicitation_enabled,
|
||||
);
|
||||
let question_id = format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}");
|
||||
let rendered_template = render_mcp_tool_approval_template(
|
||||
&invocation.server,
|
||||
metadata.and_then(|metadata| metadata.connector_id.as_deref()),
|
||||
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
|
||||
metadata.and_then(|metadata| metadata.tool_title.as_deref()),
|
||||
invocation.arguments.as_ref(),
|
||||
);
|
||||
let tool_params_display = rendered_template
|
||||
.as_ref()
|
||||
.map(|rendered_template| rendered_template.tool_params_display.clone())
|
||||
.or_else(|| build_mcp_tool_approval_display_params(invocation.arguments.as_ref()));
|
||||
let mut question = build_mcp_tool_approval_question(
|
||||
question_id.clone(),
|
||||
&invocation.server,
|
||||
&invocation.tool,
|
||||
metadata.and_then(|metadata| metadata.tool_title.as_deref()),
|
||||
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
|
||||
annotations,
|
||||
prompt_options,
|
||||
rendered_template
|
||||
.as_ref()
|
||||
.map(|rendered_template| rendered_template.question.as_str()),
|
||||
);
|
||||
question.question =
|
||||
mcp_tool_approval_question_text(question.question, monitor_reason.as_deref());
|
||||
|
|
@ -521,11 +546,22 @@ async fn maybe_request_mcp_tool_approval(
|
|||
let params = build_mcp_tool_approval_elicitation_request(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
&invocation.server,
|
||||
metadata,
|
||||
invocation.arguments.as_ref(),
|
||||
question.clone(),
|
||||
prompt_options,
|
||||
McpToolApprovalElicitationRequest {
|
||||
server: &invocation.server,
|
||||
metadata,
|
||||
tool_params: rendered_template
|
||||
.as_ref()
|
||||
.and_then(|rendered_template| rendered_template.tool_params.as_ref())
|
||||
.or(invocation.arguments.as_ref()),
|
||||
tool_params_display: tool_params_display.as_deref(),
|
||||
question,
|
||||
message_override: rendered_template.as_ref().and_then(|rendered_template| {
|
||||
monitor_reason
|
||||
.is_none()
|
||||
.then_some(rendered_template.elicitation_message.as_str())
|
||||
}),
|
||||
prompt_options,
|
||||
},
|
||||
);
|
||||
let decision = parse_mcp_tool_approval_elicitation_response(
|
||||
sess.request_mcp_server_elicitation(turn_context.as_ref(), request_id, params)
|
||||
|
|
@ -738,34 +774,16 @@ fn build_mcp_tool_approval_question(
|
|||
question_id: String,
|
||||
server: &str,
|
||||
tool_name: &str,
|
||||
tool_title: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
annotations: Option<&ToolAnnotations>,
|
||||
prompt_options: McpToolApprovalPromptOptions,
|
||||
question_override: Option<&str>,
|
||||
) -> RequestUserInputQuestion {
|
||||
let destructive =
|
||||
annotations.and_then(|annotations| annotations.destructive_hint) == Some(true);
|
||||
let open_world = annotations.and_then(|annotations| annotations.open_world_hint) == Some(true);
|
||||
let reason = match (destructive, open_world) {
|
||||
(true, true) => "may modify data and access external systems",
|
||||
(true, false) => "may modify or delete data",
|
||||
(false, true) => "may access external systems",
|
||||
(false, false) => "may have side effects",
|
||||
};
|
||||
|
||||
let tool_label = tool_title.unwrap_or(tool_name);
|
||||
let app_label = connector_name
|
||||
.map(|name| format!("The {name} app"))
|
||||
let question = question_override
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| {
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
"This app".to_string()
|
||||
} else {
|
||||
format!("The {server} MCP server")
|
||||
}
|
||||
build_mcp_tool_approval_fallback_message(server, tool_name, connector_name)
|
||||
});
|
||||
let question = format!(
|
||||
"{app_label} wants to run the tool \"{tool_label}\", which {reason}. Allow this action?"
|
||||
);
|
||||
let question = format!("{}?", question.trim_end_matches('?'));
|
||||
|
||||
let mut options = vec![RequestUserInputQuestionOption {
|
||||
label: MCP_TOOL_APPROVAL_ACCEPT.to_string(),
|
||||
|
|
@ -798,6 +816,25 @@ fn build_mcp_tool_approval_question(
|
|||
}
|
||||
}
|
||||
|
||||
fn build_mcp_tool_approval_fallback_message(
|
||||
server: &str,
|
||||
tool_name: &str,
|
||||
connector_name: Option<&str>,
|
||||
) -> String {
|
||||
let actor = connector_name
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| {
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
"this app".to_string()
|
||||
} else {
|
||||
format!("the {server} MCP server")
|
||||
}
|
||||
});
|
||||
format!("Allow {actor} to run tool \"{tool_name}\"?")
|
||||
}
|
||||
|
||||
fn mcp_tool_approval_question_text(question: String, monitor_reason: Option<&str>) -> String {
|
||||
match monitor_reason.map(str::trim) {
|
||||
Some(reason) if !reason.is_empty() => {
|
||||
|
|
@ -819,30 +856,24 @@ fn arc_monitor_interrupt_message(reason: &str) -> String {
|
|||
fn build_mcp_tool_approval_elicitation_request(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
server: &str,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
tool_params: Option<&serde_json::Value>,
|
||||
question: RequestUserInputQuestion,
|
||||
prompt_options: McpToolApprovalPromptOptions,
|
||||
request: McpToolApprovalElicitationRequest<'_>,
|
||||
) -> McpServerElicitationRequestParams {
|
||||
let message = if question.header.trim().is_empty() {
|
||||
question.question
|
||||
} else {
|
||||
let header = question.header;
|
||||
let prompt = question.question;
|
||||
format!("{header}\n\n{prompt}")
|
||||
};
|
||||
let message = request
|
||||
.message_override
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| request.question.question.clone());
|
||||
|
||||
McpServerElicitationRequestParams {
|
||||
thread_id: sess.conversation_id.to_string(),
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
server_name: server.to_string(),
|
||||
server_name: request.server.to_string(),
|
||||
request: McpServerElicitationRequest::Form {
|
||||
meta: build_mcp_tool_approval_elicitation_meta(
|
||||
server,
|
||||
metadata,
|
||||
tool_params,
|
||||
prompt_options,
|
||||
request.server,
|
||||
request.metadata,
|
||||
request.tool_params,
|
||||
request.tool_params_display,
|
||||
request.prompt_options,
|
||||
),
|
||||
message,
|
||||
requested_schema: McpElicitationSchema {
|
||||
|
|
@ -859,6 +890,7 @@ fn build_mcp_tool_approval_elicitation_meta(
|
|||
server: &str,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
tool_params: Option<&serde_json::Value>,
|
||||
tool_params_display: Option<&[RenderedMcpToolApprovalParam]>,
|
||||
prompt_options: McpToolApprovalPromptOptions,
|
||||
) -> Option<serde_json::Value> {
|
||||
let mut meta = serde_json::Map::new();
|
||||
|
|
@ -941,9 +973,34 @@ fn build_mcp_tool_approval_elicitation_meta(
|
|||
tool_params.clone(),
|
||||
);
|
||||
}
|
||||
if let Some(tool_params_display) = tool_params_display
|
||||
&& let Ok(tool_params_display) = serde_json::to_value(tool_params_display)
|
||||
{
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY.to_string(),
|
||||
tool_params_display,
|
||||
);
|
||||
}
|
||||
(!meta.is_empty()).then_some(serde_json::Value::Object(meta))
|
||||
}
|
||||
|
||||
fn build_mcp_tool_approval_display_params(
|
||||
tool_params: Option<&serde_json::Value>,
|
||||
) -> Option<Vec<crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam>> {
|
||||
let tool_params = tool_params?.as_object()?;
|
||||
let mut display_params = tool_params
|
||||
.iter()
|
||||
.map(
|
||||
|(name, value)| crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
display_params.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
Some(display_params)
|
||||
}
|
||||
|
||||
fn parse_mcp_tool_approval_elicitation_response(
|
||||
response: Option<ElicitationResponse>,
|
||||
question_id: &str,
|
||||
|
|
@ -1280,22 +1337,92 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn approval_elicitation_request_uses_message_override_and_readable_tool_params() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"create_event",
|
||||
Some("Calendar"),
|
||||
prompt_options(true, true),
|
||||
Some("Allow Calendar to create an event?"),
|
||||
);
|
||||
|
||||
let request = build_mcp_tool_approval_elicitation_request(
|
||||
&session,
|
||||
&turn_context,
|
||||
McpToolApprovalElicitationRequest {
|
||||
server: CODEX_APPS_MCP_SERVER_NAME,
|
||||
metadata: Some(&approval_metadata(
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
Some("Manage events and schedules."),
|
||||
Some("Create Event"),
|
||||
Some("Create a calendar event."),
|
||||
)),
|
||||
tool_params: Some(&serde_json::json!({
|
||||
"Calendar": "primary",
|
||||
"Title": "Roadmap review",
|
||||
})),
|
||||
tool_params_display: None,
|
||||
question,
|
||||
message_override: Some("Allow Calendar to create an event?"),
|
||||
prompt_options: prompt_options(true, true),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
request,
|
||||
McpServerElicitationRequestParams {
|
||||
thread_id: session.conversation_id.to_string(),
|
||||
turn_id: Some(turn_context.sub_id),
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
request: McpServerElicitationRequest::Form {
|
||||
meta: Some(serde_json::json!({
|
||||
MCP_TOOL_APPROVAL_KIND_KEY: MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL,
|
||||
MCP_TOOL_APPROVAL_PERSIST_KEY: [
|
||||
MCP_TOOL_APPROVAL_PERSIST_SESSION,
|
||||
MCP_TOOL_APPROVAL_PERSIST_ALWAYS,
|
||||
],
|
||||
MCP_TOOL_APPROVAL_SOURCE_KEY: MCP_TOOL_APPROVAL_SOURCE_CONNECTOR,
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY: "calendar",
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY: "Calendar",
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY: "Manage events and schedules.",
|
||||
MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: "Create Event",
|
||||
MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY: "Create a calendar event.",
|
||||
MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY: {
|
||||
"Calendar": "primary",
|
||||
"Title": "Roadmap review",
|
||||
},
|
||||
})),
|
||||
message: "Allow Calendar to create an event?".to_string(),
|
||||
requested_schema: McpElicitationSchema {
|
||||
schema_uri: None,
|
||||
type_: McpElicitationObjectType::Object,
|
||||
properties: BTreeMap::new(),
|
||||
required: None,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_mcp_tool_question_mentions_server_name() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
"custom_server",
|
||||
"run_action",
|
||||
Some("Run Action"),
|
||||
None,
|
||||
Some(&annotations(Some(false), Some(true), None)),
|
||||
prompt_options(false, false),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(question.header, "Approve app tool call?");
|
||||
assert_eq!(
|
||||
question.question,
|
||||
"The custom_server MCP server wants to run the tool \"Run Action\", which may modify or delete data. Allow this action?"
|
||||
"Allow the custom_server MCP server to run tool \"run_action\"?"
|
||||
);
|
||||
assert!(
|
||||
!question
|
||||
|
|
@ -1308,21 +1435,19 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_tool_question_keeps_legacy_app_label() {
|
||||
fn codex_apps_tool_question_uses_fallback_app_label() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"run_action",
|
||||
Some("Run Action"),
|
||||
None,
|
||||
Some(&annotations(Some(false), Some(true), None)),
|
||||
prompt_options(true, true),
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(
|
||||
question
|
||||
.question
|
||||
.starts_with("This app wants to run the tool \"Run Action\"")
|
||||
assert_eq!(
|
||||
question.question,
|
||||
"Allow this app to run tool \"run_action\"?"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1332,10 +1457,9 @@ mod tests {
|
|||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"run_action",
|
||||
Some("Run Action"),
|
||||
Some("Calendar"),
|
||||
Some(&annotations(Some(false), Some(true), None)),
|
||||
prompt_options(true, true),
|
||||
None,
|
||||
);
|
||||
let options = question.options.expect("options");
|
||||
|
||||
|
|
@ -1374,10 +1498,9 @@ mod tests {
|
|||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"run_action",
|
||||
Some("Run Action"),
|
||||
Some("Calendar"),
|
||||
Some(&annotations(Some(false), Some(true), None)),
|
||||
mcp_tool_approval_prompt_options(Some(&session_key), Some(&persistent_key), false),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -1401,10 +1524,9 @@ mod tests {
|
|||
"q".to_string(),
|
||||
"custom_server",
|
||||
"run_action",
|
||||
Some("Run Action"),
|
||||
None,
|
||||
Some(&annotations(Some(false), Some(true), None)),
|
||||
prompt_options(true, false),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -1553,6 +1675,7 @@ mod tests {
|
|||
"custom_server",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
prompt_options(false, false),
|
||||
),
|
||||
Some(serde_json::json!({
|
||||
|
|
@ -1574,6 +1697,7 @@ mod tests {
|
|||
Some("Runs the selected action."),
|
||||
)),
|
||||
Some(&serde_json::json!({"id": 1})),
|
||||
None,
|
||||
prompt_options(true, false),
|
||||
),
|
||||
Some(serde_json::json!({
|
||||
|
|
@ -1732,6 +1856,7 @@ mod tests {
|
|||
Some(&serde_json::json!({
|
||||
"calendar_id": "primary",
|
||||
})),
|
||||
None,
|
||||
prompt_options(false, false),
|
||||
),
|
||||
Some(serde_json::json!({
|
||||
|
|
@ -1764,6 +1889,7 @@ mod tests {
|
|||
Some(&serde_json::json!({
|
||||
"calendar_id": "primary",
|
||||
})),
|
||||
None,
|
||||
prompt_options(true, true),
|
||||
),
|
||||
Some(serde_json::json!({
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ use crate::bottom_pane::selection_popup_common::menu_surface_padding_height;
|
|||
use crate::bottom_pane::selection_popup_common::render_menu_surface;
|
||||
use crate::bottom_pane::selection_popup_common::render_rows;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::text_formatting::format_json_compact;
|
||||
use crate::text_formatting::truncate_text;
|
||||
|
||||
const ANSWER_PLACEHOLDER: &str = "Type your answer";
|
||||
const OPTIONAL_ANSWER_PLACEHOLDER: &str = "Type your answer (optional)";
|
||||
|
|
@ -58,6 +60,10 @@ const APPROVAL_META_KIND_TOOL_SUGGESTION: &str = "tool_suggestion";
|
|||
const APPROVAL_PERSIST_KEY: &str = "persist";
|
||||
const APPROVAL_PERSIST_SESSION_VALUE: &str = "session";
|
||||
const APPROVAL_PERSIST_ALWAYS_VALUE: &str = "always";
|
||||
const APPROVAL_TOOL_PARAMS_KEY: &str = "tool_params";
|
||||
const APPROVAL_TOOL_PARAMS_DISPLAY_KEY: &str = "tool_params_display";
|
||||
const APPROVAL_TOOL_PARAM_DISPLAY_LIMIT: usize = 3;
|
||||
const APPROVAL_TOOL_PARAM_VALUE_TRUNCATE_GRAPHEMES: usize = 60;
|
||||
const TOOL_TYPE_KEY: &str = "tool_type";
|
||||
const TOOL_ID_KEY: &str = "tool_id";
|
||||
const TOOL_NAME_KEY: &str = "tool_name";
|
||||
|
|
@ -146,12 +152,19 @@ pub(crate) struct ToolSuggestionRequest {
|
|||
pub(crate) install_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct McpToolApprovalDisplayParam {
|
||||
name: String,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct McpServerElicitationFormRequest {
|
||||
thread_id: ThreadId,
|
||||
server_name: String,
|
||||
request_id: McpRequestId,
|
||||
message: String,
|
||||
approval_display_params: Vec<McpToolApprovalDisplayParam>,
|
||||
response_mode: McpServerElicitationResponseMode,
|
||||
fields: Vec<McpServerElicitationField>,
|
||||
tool_suggestion: Option<ToolSuggestionRequest>,
|
||||
|
|
@ -216,6 +229,11 @@ impl McpServerElicitationFormRequest {
|
|||
});
|
||||
let is_tool_approval_action =
|
||||
is_tool_approval && (requested_schema.is_null() || is_empty_object_schema);
|
||||
let approval_display_params = if is_tool_approval_action {
|
||||
parse_tool_approval_display_params(meta.as_ref())
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let (response_mode, fields) = if tool_suggestion.is_some()
|
||||
&& (requested_schema.is_null() || is_empty_object_schema)
|
||||
|
|
@ -297,6 +315,7 @@ impl McpServerElicitationFormRequest {
|
|||
server_name: request.server_name,
|
||||
request_id: request.id,
|
||||
message,
|
||||
approval_display_params,
|
||||
response_mode,
|
||||
fields,
|
||||
tool_suggestion,
|
||||
|
|
@ -376,6 +395,99 @@ fn tool_approval_supports_persist_mode(meta: Option<&Value>, expected_mode: &str
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_tool_approval_display_params(meta: Option<&Value>) -> Vec<McpToolApprovalDisplayParam> {
|
||||
let Some(meta) = meta.and_then(Value::as_object) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let display_params = meta
|
||||
.get(APPROVAL_TOOL_PARAMS_DISPLAY_KEY)
|
||||
.and_then(Value::as_array)
|
||||
.map(|display_params| {
|
||||
display_params
|
||||
.iter()
|
||||
.filter_map(parse_tool_approval_display_param)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if !display_params.is_empty() {
|
||||
return display_params;
|
||||
}
|
||||
|
||||
let mut fallback_params = meta
|
||||
.get(APPROVAL_TOOL_PARAMS_KEY)
|
||||
.and_then(Value::as_object)
|
||||
.map(|tool_params| {
|
||||
tool_params
|
||||
.iter()
|
||||
.map(|(name, value)| McpToolApprovalDisplayParam {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
fallback_params.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
fallback_params
|
||||
}
|
||||
|
||||
fn parse_tool_approval_display_param(value: &Value) -> Option<McpToolApprovalDisplayParam> {
|
||||
let value = value.as_object()?;
|
||||
let name = value.get("name")?.as_str()?.trim();
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(McpToolApprovalDisplayParam {
|
||||
name: name.to_string(),
|
||||
value: value.get("value")?.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn format_tool_approval_display_message(
|
||||
message: &str,
|
||||
approval_display_params: &[McpToolApprovalDisplayParam],
|
||||
) -> String {
|
||||
let message = message.trim();
|
||||
if approval_display_params.is_empty() {
|
||||
return message.to_string();
|
||||
}
|
||||
|
||||
let mut sections = Vec::new();
|
||||
if !message.is_empty() {
|
||||
sections.push(message.to_string());
|
||||
}
|
||||
let param_lines = approval_display_params
|
||||
.iter()
|
||||
.take(APPROVAL_TOOL_PARAM_DISPLAY_LIMIT)
|
||||
.map(format_tool_approval_display_param_line)
|
||||
.collect::<Vec<_>>();
|
||||
if !param_lines.is_empty() {
|
||||
sections.push(param_lines.join("\n"));
|
||||
}
|
||||
let mut message = sections.join("\n\n");
|
||||
message.push('\n');
|
||||
message
|
||||
}
|
||||
|
||||
fn format_tool_approval_display_param_line(param: &McpToolApprovalDisplayParam) -> String {
|
||||
format!(
|
||||
"{}: {}",
|
||||
param.name,
|
||||
format_tool_approval_display_param_value(¶m.value)
|
||||
)
|
||||
}
|
||||
|
||||
fn format_tool_approval_display_param_value(value: &Value) -> String {
|
||||
let formatted = match value {
|
||||
Value::String(text) => text.split_whitespace().collect::<Vec<_>>().join(" "),
|
||||
_ => {
|
||||
let compact_json = value.to_string();
|
||||
format_json_compact(&compact_json).unwrap_or(compact_json)
|
||||
}
|
||||
};
|
||||
truncate_text(&formatted, APPROVAL_TOOL_PARAM_VALUE_TRUNCATE_GRAPHEMES)
|
||||
}
|
||||
|
||||
fn parse_fields_from_schema(requested_schema: &Value) -> Option<Vec<McpServerElicitationField>> {
|
||||
let schema = requested_schema.as_object()?;
|
||||
if schema.get("type").and_then(Value::as_str) != Some("object") {
|
||||
|
|
@ -779,12 +891,16 @@ impl McpServerElicitationOverlay {
|
|||
}
|
||||
|
||||
fn current_prompt_text(&self) -> String {
|
||||
let request_message = format_tool_approval_display_message(
|
||||
&self.request.message,
|
||||
&self.request.approval_display_params,
|
||||
);
|
||||
let Some(field) = self.current_field() else {
|
||||
return self.request.message.clone();
|
||||
return request_message;
|
||||
};
|
||||
let mut sections = Vec::new();
|
||||
if !self.request.message.trim().is_empty() {
|
||||
sections.push(self.request.message.trim().to_string());
|
||||
if !request_message.trim().is_empty() {
|
||||
sections.push(request_message);
|
||||
}
|
||||
let field_prompt = if field.label.trim().is_empty()
|
||||
|| field.prompt.trim().is_empty()
|
||||
|
|
@ -1549,7 +1665,11 @@ mod tests {
|
|||
})
|
||||
}
|
||||
|
||||
fn tool_approval_meta(persist_modes: &[&str]) -> Option<Value> {
|
||||
fn tool_approval_meta(
|
||||
persist_modes: &[&str],
|
||||
tool_params: Option<Value>,
|
||||
tool_params_display: Option<Vec<(&str, Value)>>,
|
||||
) -> Option<Value> {
|
||||
let mut meta = serde_json::Map::from_iter([(
|
||||
APPROVAL_META_KIND_KEY.to_string(),
|
||||
Value::String(APPROVAL_META_KIND_MCP_TOOL_CALL.to_string()),
|
||||
|
|
@ -1565,6 +1685,25 @@ mod tests {
|
|||
),
|
||||
);
|
||||
}
|
||||
if let Some(tool_params) = tool_params {
|
||||
meta.insert(APPROVAL_TOOL_PARAMS_KEY.to_string(), tool_params);
|
||||
}
|
||||
if let Some(tool_params_display) = tool_params_display {
|
||||
meta.insert(
|
||||
APPROVAL_TOOL_PARAMS_DISPLAY_KEY.to_string(),
|
||||
Value::Array(
|
||||
tool_params_display
|
||||
.into_iter()
|
||||
.map(|(name, value)| {
|
||||
serde_json::json!({
|
||||
"name": name,
|
||||
"value": value,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
Some(Value::Object(meta))
|
||||
}
|
||||
|
||||
|
|
@ -1616,6 +1755,7 @@ mod tests {
|
|||
server_name: "server-1".to_string(),
|
||||
request_id: McpRequestId::String("request-1".to_string()),
|
||||
message: "Allow this request?".to_string(),
|
||||
approval_display_params: Vec::new(),
|
||||
response_mode: McpServerElicitationResponseMode::FormContent,
|
||||
fields: vec![McpServerElicitationField {
|
||||
id: "confirmed".to_string(),
|
||||
|
|
@ -1681,6 +1821,7 @@ mod tests {
|
|||
server_name: "server-1".to_string(),
|
||||
request_id: McpRequestId::String("request-1".to_string()),
|
||||
message: "Allow this request?".to_string(),
|
||||
approval_display_params: Vec::new(),
|
||||
response_mode: McpServerElicitationResponseMode::ApprovalAction,
|
||||
fields: vec![McpServerElicitationField {
|
||||
id: APPROVAL_FIELD_ID.to_string(),
|
||||
|
|
@ -1723,7 +1864,7 @@ mod tests {
|
|||
form_request(
|
||||
"Allow this request?",
|
||||
empty_object_schema(),
|
||||
tool_approval_meta(&[]),
|
||||
tool_approval_meta(&[], None, None),
|
||||
),
|
||||
)
|
||||
.expect("expected approval fallback");
|
||||
|
|
@ -1735,6 +1876,7 @@ mod tests {
|
|||
server_name: "server-1".to_string(),
|
||||
request_id: McpRequestId::String("request-1".to_string()),
|
||||
message: "Allow this request?".to_string(),
|
||||
approval_display_params: Vec::new(),
|
||||
response_mode: McpServerElicitationResponseMode::ApprovalAction,
|
||||
fields: vec![McpServerElicitationField {
|
||||
id: APPROVAL_FIELD_ID.to_string(),
|
||||
|
|
@ -1805,6 +1947,43 @@ mod tests {
|
|||
assert_eq!(request, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_approval_display_params_prefer_explicit_display_order() {
|
||||
let request = McpServerElicitationFormRequest::from_event(
|
||||
ThreadId::default(),
|
||||
form_request(
|
||||
"Allow Calendar to create an event",
|
||||
empty_object_schema(),
|
||||
tool_approval_meta(
|
||||
&[],
|
||||
Some(serde_json::json!({
|
||||
"zeta": 3,
|
||||
"alpha": 1,
|
||||
})),
|
||||
Some(vec![
|
||||
("Calendar", Value::String("primary".to_string())),
|
||||
("Title", Value::String("Roadmap review".to_string())),
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
.expect("expected approval fallback");
|
||||
|
||||
assert_eq!(
|
||||
request.approval_display_params,
|
||||
vec![
|
||||
McpToolApprovalDisplayParam {
|
||||
name: "Calendar".to_string(),
|
||||
value: Value::String("primary".to_string()),
|
||||
},
|
||||
McpToolApprovalDisplayParam {
|
||||
name: "Title".to_string(),
|
||||
value: Value::String("Roadmap review".to_string()),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_sends_accept_with_typed_content() {
|
||||
let (tx, mut rx) = test_sender();
|
||||
|
|
@ -1865,10 +2044,14 @@ mod tests {
|
|||
form_request(
|
||||
"Allow this request?",
|
||||
empty_object_schema(),
|
||||
tool_approval_meta(&[
|
||||
APPROVAL_PERSIST_SESSION_VALUE,
|
||||
APPROVAL_PERSIST_ALWAYS_VALUE,
|
||||
]),
|
||||
tool_approval_meta(
|
||||
&[
|
||||
APPROVAL_PERSIST_SESSION_VALUE,
|
||||
APPROVAL_PERSIST_ALWAYS_VALUE,
|
||||
],
|
||||
None,
|
||||
None,
|
||||
),
|
||||
),
|
||||
)
|
||||
.expect("expected approval fallback");
|
||||
|
|
@ -1912,10 +2095,14 @@ mod tests {
|
|||
form_request(
|
||||
"Allow this request?",
|
||||
empty_object_schema(),
|
||||
tool_approval_meta(&[
|
||||
APPROVAL_PERSIST_SESSION_VALUE,
|
||||
APPROVAL_PERSIST_ALWAYS_VALUE,
|
||||
]),
|
||||
tool_approval_meta(
|
||||
&[
|
||||
APPROVAL_PERSIST_SESSION_VALUE,
|
||||
APPROVAL_PERSIST_ALWAYS_VALUE,
|
||||
],
|
||||
None,
|
||||
None,
|
||||
),
|
||||
),
|
||||
)
|
||||
.expect("expected approval fallback");
|
||||
|
|
@ -2105,7 +2292,7 @@ mod tests {
|
|||
form_request(
|
||||
"Allow this request?",
|
||||
empty_object_schema(),
|
||||
tool_approval_meta(&[]),
|
||||
tool_approval_meta(&[], None, None),
|
||||
),
|
||||
)
|
||||
.expect("expected approval fallback");
|
||||
|
|
@ -2125,10 +2312,14 @@ mod tests {
|
|||
form_request(
|
||||
"Allow this request?",
|
||||
empty_object_schema(),
|
||||
tool_approval_meta(&[
|
||||
APPROVAL_PERSIST_SESSION_VALUE,
|
||||
APPROVAL_PERSIST_ALWAYS_VALUE,
|
||||
]),
|
||||
tool_approval_meta(
|
||||
&[
|
||||
APPROVAL_PERSIST_SESSION_VALUE,
|
||||
APPROVAL_PERSIST_ALWAYS_VALUE,
|
||||
],
|
||||
None,
|
||||
None,
|
||||
),
|
||||
),
|
||||
)
|
||||
.expect("expected approval fallback");
|
||||
|
|
@ -2139,4 +2330,41 @@ mod tests {
|
|||
render_snapshot(&overlay, Rect::new(0, 0, 120, 16))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_form_tool_approval_with_param_summary_snapshot() {
|
||||
let (tx, _rx) = test_sender();
|
||||
let request = McpServerElicitationFormRequest::from_event(
|
||||
ThreadId::default(),
|
||||
form_request(
|
||||
"Allow Calendar to create an event",
|
||||
empty_object_schema(),
|
||||
tool_approval_meta(
|
||||
&[],
|
||||
Some(serde_json::json!({
|
||||
"calendar_id": "primary",
|
||||
"title": "Roadmap review",
|
||||
"notes": "This is a deliberately long note that should truncate before it turns the approval body into a giant wall of text in the TUI overlay.",
|
||||
"ignored_after_limit": "fourth param",
|
||||
})),
|
||||
Some(vec![
|
||||
("Calendar", Value::String("primary".to_string())),
|
||||
("Title", Value::String("Roadmap review".to_string())),
|
||||
(
|
||||
"Notes",
|
||||
Value::String("This is a deliberately long note that should truncate before it turns the approval body into a giant wall of text in the TUI overlay.".to_string()),
|
||||
),
|
||||
("Ignored", Value::String("fourth param".to_string())),
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
.expect("expected approval fallback");
|
||||
let overlay = McpServerElicitationOverlay::new(request, tx, true, false, false);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
"mcp_server_elicitation_approval_form_with_param_summary",
|
||||
render_snapshot(&overlay, Rect::new(0, 0, 120, 16))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
source: tui/src/bottom_pane/mcp_server_elicitation.rs
|
||||
expression: "render_snapshot(&overlay, Rect::new(0, 0, 120, 16))"
|
||||
---
|
||||
|
||||
Field 1/1
|
||||
Allow Calendar to create an event
|
||||
|
||||
Calendar: primary
|
||||
Title: Roadmap review
|
||||
Notes: This is a deliberately long note that should truncate bef...
|
||||
|
||||
› 1. Allow Run the tool and continue.
|
||||
2. Cancel Cancel this tool call
|
||||
|
||||
|
||||
|
||||
|
||||
enter to submit | esc to cancel
|
||||
Loading…
Add table
Reference in a new issue