MCP Workspace Budget – Sicherheitsgrenzen
Übersicht
WorkspaceMcpBudget (packages/core/src/tools/mcp-workspace-budget.ts) ist der arbeitsbereichsbezogene Budget-Controller für den MCP-Client aus F2 (#4175 Commit 6). Er besitzt die gleiche Zustandsmaschine, die McpClientManager inline mitführt (Slot-Reservierung, 75%-Hysterese-Warnung, verweigerte Batch-Zusammenführung über einen discoverAllMcpTools*-Durchlauf), lebt jedoch einmal pro Arbeitsbereich innerhalb von McpTransportPool anstatt einmal pro Session innerhalb des Managers jedes ACP-Kindes. Der Pool delegiert acquire- und release-Aufrufe hierhin, sodass die Obergrenze für den Arbeitsbereich gilt, nicht für jede Session.
Die veraltete Budget-Mechanik von McpClientManager bleibt für eigenständige Qwen- und SDK-MCP-Server bestehen (die den Pool gemäß Commit 4 umgehen). Pool-Modus → WorkspaceMcpBudget setzt durch; eigenständig/SDK-MCP → die Inline-Mechanik des Managers setzt durch. Keine Doppelzählung, da die Pool-Modus-Erkennung niemals tryReserveSlot des Managers aufruft.
Verantwortlichkeiten
- Verfolgt
reservedSlots: Set<string>der aktuell gehaltenen Server-NAMEN (Slot-Key ist pro NAME, wie in PR 14 v1). tryReserve(name) → 'reserved' | 'already_held' | 'refused'– atomar und synchron, sodass gleichzeitigePromise.all-Acquires die Obergrenze nicht an einerawait-Grenze überschreiten können.release(name) → boolean– idempotent (Semantik vonSet.delete).- Löst
mcp_budget_warningeinmalig beim Überschreiten von 75% vonreservedSlots.size / clientBudgetaus; erneut aktiviert erst nach einem Unterschreiten von 37,5%. - Fasst serverbezogene Ablehnungen über einen Bulk-Erkennungsdurchlauf zusammen –
beginBulkPass()/endBulkPass()Klammern, um Ablehnungen in einem einzigenmcp_child_refused_batch-Ereignis zu sammeln. - Pflegt
lastRefusedServerNamesfür Snapshot-Konsumenten (GET /workspace/mcp) – wird zu BEGINN des nächsten Bulk-Durchlaufs gelöscht, NICHT beim Senden, sodass ein Snapshot zwischen zwei Durchläufen immer noch den letzten Satz abgelehnter Server zeigt.
Architektur
Konfiguration
new WorkspaceMcpBudget({
clientBudget?: number, // undefined = unbegrenzt
mode: 'off' | 'warn' | 'enforce',
onEvent?: (event: McpBudgetEvent) => void,
});Semantik von mode:
off– jede Methode ist ein No-Op;tryReservegibt immer'reserved'zurück; es werden keine Ereignisse ausgelöst.warn– Slots werden verfolgt undmcp_budget_warningwird bei 75% ausgelöst, abertryReservelehnt NIE ab.enforce–tryReservelehnt ab, sobaldclientBudgeterreicht ist;recordRefusalreiht serverbezogene Ablehnungen ein;endBulkPasssendetmcp_child_refused_batch.
Konstanten aus mcp-client-manager.ts
MCP_BUDGET_WARN_FRACTION = 0.75– Aufwärtsschwelle.MCP_BUDGET_REARM_FRACTION = 0.375– Abwärts-Hysterese zur erneuten Aktivierung.McpBudgetMode = 'off' | 'warn' | 'enforce'.
Interner Zustand
| Zustand | Zweck |
|---|---|
reservedSlots: Set<string> | Maßgeblicher Reservierungssatz; Hysterese bewertet size / clientBudget. |
pendingRefusalNames: Set<string> | Namen von Ablehnungen, die während des aktuellen beginBulkPass/endBulkPass-Fensters gesammelt wurden; werden bei endBulkPass geleert. |
pendingRefusalTransports: Map<string, transport> | Seitenwagon, damit der gesendete Batch den Transport jedes abgelehnten Servers enthält. |
lastRefusedServerNames: readonly string[] | Snapshot-sichtbare Ablehnungsliste des letzten abgeschlossenen Durchlaufs. Wird zu Beginn des nächsten Durchlaufs gelöscht. |
warnArmed: boolean | Hysterese-Zustand – true = bereit zum Auslösen, false = bereits ausgelöst seit letztem 37,5%-Abfluss. |
bulkPassDepth: number | Wiedereintrittszähler für verschachtelte Bulk-Durchläufe (verschachtelte Durchläufe dürfen nicht doppelt senden). |
Arbeitsablauf
tryReserve
tryReserve ist synchron. Der acquire-Aufruf des Pools ist asynchron, aber die Reservierung erfolgt vor jedem await, sodass zwei gleichzeitige Promise.all-Acquires für verschiedene Namen die Obergrenze nicht beide überschreiten können.
Hysterese
Hysterese vermeidet wiederholte Warnungen, wenn eine Arbeitslast um 75% schwankt. Die erste Überschreitung feuert; nachfolgende Überschreitungen ohne einen Abfall auf 37,5% lösen keine Warnung aus.
Zusammenfassen abgelehnter Batches
Ablehnungen außerhalb eines Durchgangs (z. B. ein faules readResource-Spawn, das den gesamten Batch-Durchgang umgeht) geben aus Konsistenzgründen Batches der Länge 1 inline aus. Verschachtelte Durchgänge (bulkPassDepth > 0) feuern nicht; nur der äußerste Durchgangs-Abschluss gibt den zusammengefassten Batch aus.
Zustand & Lebenszyklus
- Der Budget-Controller wird einmal pro Workspace beim Pool-Initialisieren erstellt.
clientBudgetist nach der Erstellung unveränderlich; Laufzeitänderungen erfordern eine Neuerstellung des Pools.modeist ebenfalls unveränderlich (onEventwird alsundefinedgespeichert, wennmode === 'off'– Defence-in-Depth).warnArmedbeginnt alstrue; wird durch die 37,5 %-Abwärtskreuzung auftruezurückgesetzt.lastRefusedServerNameswird beimendBulkPass-Emit NICHT gelöscht – erst zu BEGINN des nächsten Bulk-Durchgangs. So kann eine zwischen zwei Durchgängen aufgerufene Snapshot-Route immer noch den letzten Satz abgelehnter Server melden (andernfalls würden Dashboards direkt nach der Zustellung eines abgelehnten Batch-Ereignisses leere Ablehnungen anzeigen).
Abhängigkeiten
packages/core/src/tools/mcp-client-manager.ts– verwendetMcpBudgetEvent,McpBudgetMode,McpRefusedServer,MCP_BUDGET_WARN_FRACTION,MCP_BUDGET_REARM_FRACTION,BudgetExhaustedError(wird vom Pool beiacquirebei Ablehnung geworfen) wieder.packages/core/src/tools/mcp-transport-pool.ts– konsumiert das Budget; leitet Ereignisse über dieonEvent-Verkabelung des Pools an den Daemon-EventBus weiter.- Daemon-Snapshot-Route
GET /workspace/mcp– liestgetReservedSlots(),getRefusedServerNames(),getReservedCount(),getBudget(),getMode()aus.
Konfiguration
| Quelle | Einstellung | Wirkung |
|---|---|---|
| Flag | --mcp-client-budget=N | Setzt clientBudget für den Workspace-Controller. |
| Flag | --mcp-budget-mode={off,warn,enforce} | Setzt mode. enforce erfordert ein positives clientBudget; andernfalls schlägt der Start explizit fehl. |
| Umgebungsvar. | QWEN_SERVE_MCP_CLIENT_BUDGET, QWEN_SERVE_MCP_BUDGET_MODE | Wird über childEnvOverrides an das ACP-Kind weitergeleitet; readBudgetFromEnv() des Kindes liest sie aus. |
| Capability-Tags | mcp_guardrails (immer; modes: ['warn', 'enforce']), mcp_guardrail_events (immer) | Siehe 11-capabilities-versioning.md. |
Einschränkungen & bekannte Grenzen
- Reservierungsschlüssel ist PRO NAME. Zwei Pool-Einträge mit demselben Server-Namen, aber unterschiedlichen Fingerprints (z. B. Sessions, die abweichende OAuth-Header einfügen) belegen GEMEINSAM einen Slot. Die Subprozess-Abrechnung wird separat über die
subprocessCountdes Pool-Snapshots angezeigt. Betreiber sollten das Budget als „konfigurierte Server-Slots” betrachten, nicht als „Subprozess-Anzahl”. - Die Hysterese wird durch die Anzahl der Reservierungen ausgelöst, nicht durch die Anzahl der aktiven (CONNECTED) Verbindungen. Reservierungen umfassen laufende Verbindungsaufbauten und überstehen vorübergehende Trennungen, sodass die Hysterese über Wiederverbindungszyklen hinweg stabil bleibt. Die Live-Verbindungsanzahl wird in Ereignis-Payloads als
liveCountbereitgestellt, für SDK-Konsumenten, die diese Perspektive benötigen. - Der
warn-Modus lehnt niemals ab. Er verfolgt weiterhin Reservierungen und feuertmcp_budget_warning, abertryReservegibt immer'reserved'zurück. Die Ablehnungssemantik ist ausschließlich fürenforce. - Workspace-bezogene Budget-Ereignisse enthalten
scope: 'workspace', sodass sie gleichzeitig an jede angeschlossene Session verteilt werden. DiemcpBudgetWarningCount/mcpChildRefusedBatchCountder SDK-Reducer inkrementieren auf derselben Verbindung in allen Sessions synchron. Legacy-Ereignisse pro Session vonMcpClientManagerenthalten keinenscope(semantisch standardmäßig'session'). - Der Kill-Switch
QWEN_SERVE_NO_MCP_POOL=1deaktiviert den Pool vollständig; das Workspace-Budget wird ebenfalls deaktiviert, und das pro-Session-Budget desMcpClientManagerübernimmt. Das Capabilities-Paket entferntmcp_workspace_poolundmcp_pool_restart, um dies korrekt zu melden. ServeMcpBudgetStatusCell.scopeist eine vorwärtskompatible Listenform. Snapshot-Zellen gebenbudgets[]aus, nicht ein einzelnes Feldbudget?. PR 14 v1 gibt eine Zelle mitscope: 'session'pro ACP-Session aus, daacpAgent.newSessionConfig()dasConfig/McpClientManagerdieser Session erstellt. Der'pool'-Scope ist für die pool-bezogene Zelle des Wave 5 PR 23 reserviert, die neben Sessions-bezogenen Zellen platziert wird. Konsumenten müssen zusätzliche unbekanntescope-Werte tolerieren, indem sie sie ignorieren anstatt einen Fehler zu werfen.
Referenzen
packages/core/src/tools/mcp-workspace-budget.ts(gesamte Klasse)packages/core/src/tools/mcp-client-manager.ts(BudgetExhaustedError,McpBudgetEvent, Hysterese-Konstanten)packages/core/src/tools/mcp-transport-pool.ts(dieacquire-Stelle des Pools, dietryReserveaufruft)- F2-Design-Dokument (v2.2):
../../design/f2-mcp-transport-pool.md§11 für das Arbeitsbereichs-Budget und die v2.2-Changelog-Einträge zu Budget- und Fingerprint-Nachfassungen. - F2-Designnotizen: Issue #4175 Commit 6.