Context
Slice of #9980 (Architect MCP Capability Gating by Model Tier). #9980 wants a tiered capability matrix so a local lower-parameter worker (e.g. a Gemma Librarian) is not exposed to the full github-workflow MCP surface it cannot navigate safely.
The gap (V-B-A'd live): Agent.servers: [...] selects MCP servers all-or-nothing — Loop.initAsync (ai/agent/Loop.mjs) pushes every tool of every connected client to the model. The Librarian profile already restricts to servers: ['knowledge-base'], but there is no way to expose a subset of a server's tools (e.g. a worker limited to the signal_state_transition trap endpoint that #9980 names).
Scope (this slice — the enabling MECHANISM)
- Pure core
ai/agent/resolveAllowedTools.mjs — resolveAllowedTools({tools, allowedTools, serverName}) → the permitted subset. null = no filtering (backward-compatible); {[serverName]: String[]} = per-server opt-in allowlist. Side-effect-free, fully unit-tested.
allowedTools config on Agent.mjs (the contract) + pass-through to Loop, with the filter applied at tool-assembly so a gated-out tool is not even registered in toolRegistry (not routable).
Semantics (Tier-2, additive, reversible): fail-open per server — a server absent from a non-null map keeps its full surface; only named servers are constrained, so forgetting one is not a silent capability loss.
Out of scope (stays on parent #9980 for design-convergence)
The tier→tool policy matrix itself — which concrete tools each model tier gets, and wiring tier-specific allowlists into the real profiles (Librarian etc.) + the orchestrator that selects a tier. That is the high-blast security policy (deny-by-default vs fail-open-per-server is a policy the matrix layers on top); the mechanism here supports both. Not a unilateral build.
Acceptance
Refs #9980. Authored by @neo-opus-vega.
Context
Slice of #9980 (Architect MCP Capability Gating by Model Tier). #9980 wants a tiered capability matrix so a local lower-parameter worker (e.g. a Gemma Librarian) is not exposed to the full
github-workflowMCP surface it cannot navigate safely.The gap (V-B-A'd live):
Agent.servers: [...]selects MCP servers all-or-nothing —Loop.initAsync(ai/agent/Loop.mjs) pushes every tool of every connected client to the model. TheLibrarianprofile already restricts toservers: ['knowledge-base'], but there is no way to expose a subset of a server's tools (e.g. a worker limited to thesignal_state_transitiontrap endpoint that #9980 names).Scope (this slice — the enabling MECHANISM)
ai/agent/resolveAllowedTools.mjs—resolveAllowedTools({tools, allowedTools, serverName})→ the permitted subset.null= no filtering (backward-compatible);{[serverName]: String[]}= per-server opt-in allowlist. Side-effect-free, fully unit-tested.allowedToolsconfig onAgent.mjs(the contract) + pass-through toLoop, with the filter applied at tool-assembly so a gated-out tool is not even registered intoolRegistry(not routable).Semantics (Tier-2, additive, reversible): fail-open per server — a server absent from a non-null map keeps its full surface; only named servers are constrained, so forgetting one is not a silent capability loss.
Out of scope (stays on parent #9980 for design-convergence)
The tier→tool policy matrix itself — which concrete tools each model tier gets, and wiring tier-specific allowlists into the real profiles (Librarian etc.) + the orchestrator that selects a tier. That is the high-blast security policy (deny-by-default vs fail-open-per-server is a policy the matrix layers on top); the mechanism here supports both. Not a unilateral build.
Acceptance
resolveAllowedToolspure module + unit spec (backward-compat default, per-server opt-in, trap-endpoint subset, explicit-deny empty list, non-mutation).Agent.allowedToolsconfig +Loopwiring at tool-assembly; default null = no behavior change for existing profiles.Refs #9980. Authored by @neo-opus-vega.