|
|
@@ -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,14 +398,13 @@ 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('✅ Allow', `perm:allow:${request_id}`)
|
|
|
- .text('❌ Deny', `perm:deny:${request_id}`)
|
|
|
+ .text('See more', `perm:more:${request_id}`)
|
|
|
+ .text('✅ Yes', `perm:allow:${request_id}`)
|
|
|
+ .text('❌ No', `perm:deny:${request_id}`)
|
|
|
for (const chat_id of access.allowFrom) {
|
|
|
void bot.api.sendMessage(chat_id, text, { reply_markup: keyboard }).catch(e => {
|
|
|
process.stderr.write(`permission_request send to ${chat_id} failed: ${e}\n`)
|
|
|
@@ -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('✅ Yes', `perm:allow:${request_id}`)
|
|
|
+ .text('❌ No', `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
|