feat(telegram,discord): compact permission messages with expandable details (#952)

* feat(telegram,discord): compact permission messages with expandable details

Replace verbose permission request messages with a compact format showing
only the tool name. Adds a "See more" button that expands inline to show
tool_name, description, and pretty-printed input_preview JSON. Yes/No
buttons replace Allow/Deny. Bump plugin versions to 0.0.4.

* revert: restore Allow/Deny button labels
This commit is contained in:
Noah Zweben
2026-03-23 22:53:47 -07:00
committed by GitHub
parent b3a0714d7f
commit 4b1e2a28ce
4 changed files with 85 additions and 14 deletions

View File

@@ -379,6 +379,9 @@ const mcp = new Server(
},
)
// Stores full permission details for "See more" expansion keyed by request_id.
const pendingPermissions = new Map<string, { tool_name: string; description: string; input_preview: string }>()
// Receive permission_request from CC → format → send to all allowlisted DMs.
// Groups are intentionally excluded — the security thread resolution was
// "single-user mode for official plugins." Anyone in access.allowFrom
@@ -395,12 +398,11 @@ mcp.setNotificationHandler(
}),
async ({ params }) => {
const { request_id, tool_name, description, input_preview } = params
pendingPermissions.set(request_id, { tool_name, description, input_preview })
const access = loadAccess()
const text =
`🔐 Permission request [${request_id}]\n` +
`${tool_name}: ${description}\n` +
`${input_preview}`
const text = `🔐 Permission: ${tool_name}`
const keyboard = new InlineKeyboard()
.text('See more', `perm:more:${request_id}`)
.text('✅ Allow', `perm:allow:${request_id}`)
.text('❌ Deny', `perm:deny:${request_id}`)
for (const chat_id of access.allowFrom) {
@@ -686,11 +688,11 @@ bot.command('status', async ctx => {
})
// Inline-button handler for permission requests. Callback data is
// `perm:allow:<id>` or `perm:deny:<id>` — set when the request was sent.
// `perm:allow:<id>`, `perm:deny:<id>`, or `perm:more:<id>`.
// Security mirrors the text-reply path: allowFrom must contain the sender.
bot.on('callback_query:data', async ctx => {
const data = ctx.callbackQuery.data
const m = /^perm:(allow|deny):([a-km-z]{5})$/.exec(data)
const m = /^perm:(allow|deny|more):([a-km-z]{5})$/.exec(data)
if (!m) {
await ctx.answerCallbackQuery().catch(() => {})
return
@@ -702,10 +704,38 @@ bot.on('callback_query:data', async ctx => {
return
}
const [, behavior, request_id] = m
if (behavior === 'more') {
const details = pendingPermissions.get(request_id)
if (!details) {
await ctx.answerCallbackQuery({ text: 'Details no longer available.' }).catch(() => {})
return
}
const { tool_name, description, input_preview } = details
let prettyInput: string
try {
prettyInput = JSON.stringify(JSON.parse(input_preview), null, 2)
} catch {
prettyInput = input_preview
}
const expanded =
`🔐 Permission: ${tool_name}\n\n` +
`tool_name: ${tool_name}\n` +
`description: ${description}\n` +
`input_preview:\n${prettyInput}`
const keyboard = new InlineKeyboard()
.text('✅ Allow', `perm:allow:${request_id}`)
.text('❌ Deny', `perm:deny:${request_id}`)
await ctx.editMessageText(expanded, { reply_markup: keyboard }).catch(() => {})
await ctx.answerCallbackQuery().catch(() => {})
return
}
void mcp.notification({
method: 'notifications/claude/channel/permission',
params: { request_id, behavior },
})
pendingPermissions.delete(request_id)
const label = behavior === 'allow' ? '✅ Allowed' : '❌ Denied'
await ctx.answerCallbackQuery({ text: label }).catch(() => {})
// Replace buttons with the outcome so the same request can't be answered