fix: scope an approval bypass to the tool, not to its whole connector

Approving one tool call with "15 min" or "Session" registered a bypass whose
scope was *inferred* from the call's metadata: a registered category if it had
one, otherwise its MCP server. For a connector tool that meant the whole
connector — so approving `mcp__gmail__modify_message` (labelling, archiving:
what an assistant tidying a mailbox does constantly) silently un-gated
`mcp__gmail__send_message` for the rest of the conversation, straight through
the explicit `require` rule written for it. An email went out with no prompt;
the only trace was an INFO line, since bypasses live in RAM.

A human answering a card has read one call. That call is the widest thing the
click may authorise, so the scope is now always the tool itself and is never
guessed. The wider scopes stay in the enum and stay reachable through the REST
`bypass_scope` field, where naming one is deliberate.

Both fallbacks now narrow instead of widening: a scope that cannot be honoured
(a category-less tool, a non-MCP one) and an unknown scope string both degrade
to the tool, where they used to fall through to a session-wide bypass. Only a
literal "all" disables the gate session-wide.

The buttons said "skip similar requests" without ever defining "similar"; they
now name the tool.
This commit is contained in:
2026-08-07 13:17:57 +01:00
parent c0a779b79e
commit 548871fc72
6 changed files with 156 additions and 48 deletions
+2 -2
View File
@@ -321,8 +321,8 @@ export default {
'approval.reject': 'Deny',
'approval.confirm_reject': 'Confirm deny',
'approval.reject_hint': 'Optional: say why (the assistant will read it)',
'approval.bypass_15': 'Allow and skip similar requests for 15 minutes',
'approval.bypass_all': 'Allow and skip all requests for this session',
'approval.bypass_15': 'Allow, and stop asking for this same tool for 15 minutes',
'approval.bypass_all': 'Allow, and stop asking for this same tool for the rest of this conversation',
// ── Login ──────────────────────────────────────────────────────────────────
'login.title': 'Welcome back',
+2 -2
View File
@@ -321,8 +321,8 @@ export default {
'approval.reject': 'Refuser',
'approval.confirm_reject': 'Confirmer le refus',
'approval.reject_hint': 'Facultatif : dites pourquoi (l\'assistant le lira)',
'approval.bypass_15': 'Autoriser et ignorer les demandes similaires pendant 15 minutes',
'approval.bypass_all': 'Autoriser et ignorer toutes les demandes pour cette session',
'approval.bypass_15': 'Autoriser et ne plus demander pour ce même outil pendant 15 minutes',
'approval.bypass_all': 'Autoriser et ne plus demander pour ce même outil jusqu\'à la fin de la conversation',
// ── Login ──────────────────────────────────────────────────────────────────
'login.title': 'Bon retour',
+2 -2
View File
@@ -321,8 +321,8 @@ export default {
'approval.reject': 'Nega',
'approval.confirm_reject': 'Conferma il rifiuto',
'approval.reject_hint': 'Facoltativo: spiega perché (lo leggerà l\'assistente)',
'approval.bypass_15': 'Consenti e salta richieste simili per 15 minuti',
'approval.bypass_all': 'Consenti e salta tutte le richieste di questa sessione',
'approval.bypass_15': 'Consenti e non chiedere più per questo stesso strumento per 15 minuti',
'approval.bypass_all': 'Consenti e non chiedere più per questo stesso strumento per il resto della conversazione',
// ── Accesso ────────────────────────────────────────────────────────────────
'login.title': 'Bentornato',
+12 -9
View File
@@ -73,19 +73,22 @@ export const InboxCardsMixin = (Base) => class extends Base {
this._resolveApproval(requestId, 'reject', note, null, null, toolCallId);
}
/** Approve + set a timed or session bypass scoped to the tool's category or MCP server. */
/**
* Approve + skip approval for **this same tool** for a while.
*
* Scoped to the tool and nothing wider: the card the human just read is
* about one call, and the previous category/MCP-server auto-detect meant a
* click on "label this message" also un-gated "send this message" for the
* rest of the session.
*/
_approveWithBypass(item, bypassSecs) {
const scope = item.tool_category ? 'category'
: item.mcp_server ? 'mcp_server'
: 'all';
this._resolveApproval(item.request_id, 'approve', '', bypassSecs, scope);
this._resolveApproval(item.request_id, 'approve', '', bypassSecs, 'tool');
}
/** Human-readable bypass scope label, e.g. "filesystem" or "Gmail". */
/** Short label for the bypass scope — the tool, `mcp__x__y` shown as `y`. */
_bypassLabel(item) {
if (item.tool_category) return item.tool_category;
if (item.mcp_server) return item.mcp_server;
return 'session';
const name = item.tool_name ?? '';
return name.startsWith('mcp__') ? name.split('__').pop() : name;
}
async _resolveClarification(requestId, inputEl) {