MCP Workspace Budget Guardrails
Vue d’ensemble
WorkspaceMcpBudget (packages/core/src/tools/mcp-workspace-budget.ts) est le contrôleur de budget client MCP à l’échelle d’un espace de travail provenant de F2 (#4175 commit 6). Il possède la même machine d’état que celle que McpClientManager embarque en ligne (réservation de créneaux, avertissement à hystérésis à 75%, coalescence des refus groupés lors d’une phase discoverAllMcpTools*), mais vit une fois par espace de travail à l’intérieur de McpTransportPool au lieu d’une fois par session dans chaque gestionnaire enfant ACP. Le pool délègue les appels acquire et release ici, de sorte que la limite s’applique à l’espace de travail, pas à chaque session.
Le mécanisme de budget hérité de McpClientManager reste pour les serveurs MCP autonomes qwen et SDK (qui contournent le pool selon le correctif du commit 4). Mode pool → WorkspaceMcpBudget applique la règle ; mode autonome / SDK MCP → le mécanisme en ligne du gestionnaire applique la règle. Pas de double comptage car la découverte en mode pool n’appelle jamais tryReserveSlot du gestionnaire.
Responsabilités
- Suivi de
reservedSlots: Set<string>des NOMS de serveurs actuellement détenus (la clé de créneau est par NOM, comme dans PR 14 v1). tryReserve(name) → 'reserved' | 'already_held' | 'refused'— atomique et synchrone afin que desPromise.allconcurrents ne puissent pas dépasser la limite à une frontièreawait.release(name) → boolean— idempotent (sémantiqueSet.delete).- Déclencher
mcp_budget_warningune seule fois lors du franchissement à la hausse de 75% dereservedSlots.size / clientBudget; réarmer uniquement après un franchissement à la baisse de 37,5%. - Coalescence des refus par serveur lors d’une phase de découverte groupée —
beginBulkPass()/endBulkPass()encadrent l’accumulation des refus en un seul événementmcp_child_refused_batch. - Maintien de
lastRefusedServerNamespour les consommateurs d’instantané (GET /workspace/mcp) — effacé au DÉBUT de la phase suivante, PAS à l’émission, afin qu’un instantané entre deux phases voie toujours le dernier ensemble de refus.
Architecture
Configuration
new WorkspaceMcpBudget({
clientBudget?: number, // undefined = illimité
mode: 'off' | 'warn' | 'enforce',
onEvent?: (event: McpBudgetEvent) => void,
});Sémantique de mode :
off— toutes les méthodes sont sans effet ;tryReserveretourne'reserved'inconditionnellement ; aucun événement n’est émis.warn— les créneaux sont suivis etmcp_budget_warningse déclenche à 75%, maistryReservene refuse JAMAIS.enforce—tryReserverefuse au-delà declientBudget;recordRefusalmet en file les refus par serveur ;endBulkPassémetmcp_child_refused_batch.
Constantes provenant de mcp-client-manager.ts
MCP_BUDGET_WARN_FRACTION = 0.75— seuil de déclenchement à la hausse.MCP_BUDGET_REARM_FRACTION = 0.375— seuil de réarmement à la baisse (hystérésis).McpBudgetMode = 'off' | 'warn' | 'enforce'.
État interne
| État | Objectif |
|---|---|
reservedSlots: Set<string> | Ensemble de réservations faisant autorité ; l’hystérésis évalue size / clientBudget. |
pendingRefusalNames: Set<string> | Noms de refus accumulés pendant la fenêtre beginBulkPass/endBulkPass ; vidé dans endBulkPass. |
pendingRefusalTransports: Map<string, transport> | Complément pour que le lot émis transporte le transport de chaque serveur refusé. |
lastRefusedServerNames: readonly string[] | Liste des refus visible dans l’instantané, issue de la phase la plus récente. Effacée au début de la phase suivante. |
warnArmed: boolean | État d’hystérésis — true = prêt à déclencher, false = déjà déclenché depuis le dernier drainage à 37,5%. |
bulkPassDepth: number | Compteur de réentrance pour les phases groupées imbriquées (les phases imbriquées ne doivent pas émettre deux fois). |
Workflow
tryReserve
tryReserve est synchrone. L’appel acquire du pool est asynchrone, mais la réservation a lieu avant tout await, donc deux Promise.all concurrents pour des noms différents ne peuvent pas dépasser la limite.
Hystérésis
L’hystérésis évite les avertissements répétés lorsqu’une charge oscille autour de 75%. Le premier franchissement déclenche ; les franchissements suivants sans descendre à 37,5% ne déclenchent pas.
Coalescence des refus groupés
Les refus hors phase (par exemple un spawn readResource paresseux qui contourne totalement la phase groupée) émettent des lots de longueur 1 en ligne pour la cohérence de la forme. Les phases imbriquées (bulkPassDepth > 0) n’émettent pas ; seule la fin de la phase la plus externe émet le lot coalescé.
État & Cycle de vie
- Le contrôleur de budget est construit une fois par espace de travail à l’initialisation du pool.
clientBudgetest immuable après construction ; les modifications à l’exécution nécessitent une reconstruction du pool.modeest également immuable (onEventest mis àundefinedquandmode === 'off'comme défense en profondeur).warnArmedcommence à true ; repasse à true via le franchissement à la baisse de 37,5%.lastRefusedServerNamesn’est PAS effacé lors de l’émission deendBulkPass— seulement au DÉBUT de la phase suivante. Cela permet à une route d’instantané appelée entre deux phases de toujours rapporter le dernier ensemble de refus (sinon les tableaux de bord montreraient des refus vides immédiatement après la livraison d’un événement de refus groupé).
Dépendances
packages/core/src/tools/mcp-client-manager.ts— réutiliseMcpBudgetEvent,McpBudgetMode,McpRefusedServer,MCP_BUDGET_WARN_FRACTION,MCP_BUDGET_REARM_FRACTION,BudgetExhaustedError(lancé paracquiredu pool en cas de refus).packages/core/src/tools/mcp-transport-pool.ts— consomme le budget ; transmet les événements au bus d’événements du démon via la plomberieonEventdu pool.- Route d’instantané du démon
GET /workspace/mcp— litgetReservedSlots(),getRefusedServerNames(),getReservedCount(),getBudget(),getMode().
Configuration
| Source | Bouton de réglage | Effet |
|---|---|---|
| Flag | --mcp-client-budget=N | Définit clientBudget pour le contrôleur de l’espace de travail. |
| Flag | --mcp-budget-mode={off,warn,enforce} | Définit mode. enforce nécessite un clientBudget positif ; sinon le démarrage échoue explicitement. |
| Env | QWEN_SERVE_MCP_CLIENT_BUDGET, QWEN_SERVE_MCP_BUDGET_MODE | Transmis à l’enfant ACP via childEnvOverrides ; l’enfant les récupère avec readBudgetFromEnv(). |
| Étiquettes de capacité | mcp_guardrails (toujours ; modes: ['warn', 'enforce']), mcp_guardrail_events (toujours) | Voir 11-capabilities-versioning.md. |
Limitations & Problèmes connus
- La clé de réservation est par NOM. Deux entrées du pool avec le même nom de serveur mais des empreintes différentes (par ex. sessions injectant des en-têtes OAuth divergents) consomment UN créneau ensemble. Le comptage des sous-processus est exposé séparément via le champ
subprocessCountde l’instantané du pool. Les opérateurs doivent considérer le budget comme « créneaux de serveur configurés » et non « nombre de sous-processus ». - L’hystérésis se déclenche sur le nombre de réservations, pas sur le nombre de connexions actives (CONNECTED). Les réservations incluent les connexions en cours et survivent aux déconnexions transitoires, donc l’hystérésis reste stable pendant les cycles de reconnexion. Le nombre de connexions actives est exposé dans les charges utiles des événements sous
liveCountpour les consommateurs SDK qui souhaitent cette perspective. - Le mode
warnne refuse jamais. Il suit toujours les réservations et émetmcp_budget_warning, maistryReserveretourne toujours'reserved'. La sémantique de refus est réservée àenforce. - Les événements de budget à l’échelle de l’espace de travail portent
scope: 'workspace'afin qu’ils se propagent simultanément à chaque session attachée. Les compteursmcpBudgetWarningCount/mcpChildRefusedBatchCountdes réducteurs SDK s’incrémentent de manière synchrone entre les sessions sur la même connexion. Les événements hérités par session deMcpClientManagerne portent pas descope(par défaut, sémantique'session'). - Le coupe-circuit
QWEN_SERVE_NO_MCP_POOL=1désactive complètement le pool ; le budget de l’espace de travail est également désactivé, et le budget par session deMcpClientManagerprend le relais. L’enveloppe de capacités supprimemcp_workspace_pooletmcp_pool_restartpour signaler cela correctement. ServeMcpBudgetStatusCell.scopeest une liste de forme compatible ascendante. Les cellules d’instantané exposentbudgets[], et non un seul champbudget?. PR 14 v1 émet une cellulescope: 'session'pour chaque session ACP caracpAgent.newSessionConfig()construit leConfig/McpClientManagerde cette session. La portée'pool'est réservée à la cellule de portée pool de la Wave 5 PR 23, qui coexistera avec les cellules de portée session. Les consommateurs doivent tolérer des valeursscopeinconnues supplémentaires en les ignorant plutôt qu’en échouant.
Références
packages/core/src/tools/mcp-workspace-budget.ts(classe entière)packages/core/src/tools/mcp-client-manager.ts(BudgetExhaustedError,McpBudgetEvent, constantes d’hystérésis)packages/core/src/tools/mcp-transport-pool.ts(siteacquiredu pool qui appelletryReserve)- Document de conception F2 (v2.2) :
../../design/f2-mcp-transport-pool.md§11 pour le budget au niveau de l’espace de travail et les entrées du journal des modifications v2.2 concernant le budget et les suivis d’empreintes. - Notes de conception F2 : issue #4175 commit 6.