涉及源码 :
src/tool_pool.py、src/tools.py、src/permissions.py、src/main.py、src/system_init.py;衔接result/08.md、result/09.md、result/05.md。
1. 「可用工具面」在本仓库里的精确定义
这里说的不是哲学上的「模型理论上能用什么」,而是 某次调用 get_tools(...) / assemble_tool_pool(...) 得到的 PortingModule 元组 :
在那一刻,系统把哪些工具条目视为 允许进入池子(给 UI 列表、给报告、给未来的 tool schema 装配)。
全集 来自快照,进程内常量为 PORTED_TOOLS:
python
# 37:37:src/tools.py
PORTED_TOOLS = load_tool_snapshot()
池子 是在全集上做 一串确定性过滤 的结果;默认策略 即各开关取默认值、且无 ToolPermissionContext 时的结果。
2. 组装流水线:assemble_tool_pool → get_tools
python
# 28:37:src/tool_pool.py
def assemble_tool_pool(
simple_mode: bool = False,
include_mcp: bool = True,
permission_context: ToolPermissionContext | None = None,
) -> ToolPool:
return ToolPool(
tools=get_tools(simple_mode=simple_mode, include_mcp=include_mcp, permission_context=permission_context),
simple_mode=simple_mode,
include_mcp=include_mcp,
)
ToolPool 本身只 缓存策略参数 + 过滤后的元组 ,并用 Markdown 输出 总数与前 15 条(便于扫一眼,不全量刷屏):
python
# 16:25:src/tool_pool.py
def as_markdown(self) -> str:
lines = [
'# Tool Pool',
'',
f'Simple mode: {self.simple_mode}',
f'Include MCP: {self.include_mcp}',
f'Tool count: {len(self.tools)}',
]
lines.extend(f'- {tool.name} --- {tool.source_hint}' for tool in self.tools[:15])
return '\n'.join(lines)
CLI 默认报表 :python3 -m src.main tool-pool 调用 assemble_tool_pool() 无参 ,即 simple_mode=False、include_mcp=True、permission_context=None ------ 在代码能力上等于 「快照全集再过一遍权限过滤器(空)」 ,也就是 最大池 (与 get_tools() 默认一致)。
3. 过滤顺序与语义:get_tools 的三段闸门
python
# 62:72:src/tools.py
def get_tools(
simple_mode: bool = False,
include_mcp: bool = True,
permission_context: ToolPermissionContext | None = None,
) -> tuple[PortingModule, ...]:
tools = list(PORTED_TOOLS)
if simple_mode:
tools = [module for module in tools if module.name in {'BashTool', 'FileReadTool', 'FileEditTool'}]
if not include_mcp:
tools = [module for module in tools if 'mcp' not in module.name.lower() and 'mcp' not in module.source_hint.lower()]
return filter_tools_by_permission_context(tuple(tools), permission_context)
3.1 Simple mode:硬编码「三件套」
开启后 只保留 BashTool、FileReadTool、FileEditTool 三个 精确工具名 。
效果:可用工具面急剧收缩到「最小可讲清楚的 demo 面」,适合教学、沙箱、或先不接复杂工具生态。
边界 :名字不在集合内的「基础」工具也会被砍掉------这是 刻意极端 的产品策略占位,不是自动推断「安全工具」。
3.2 MCP 开关:子串启发式
include_mcp=False 时,丢弃满足以下任一条件的条目:
module.name.lower()含mcp,或module.source_hint.lower()含mcp。
效果 :把 MCP 相关工具族 从整体池里剥离(例如 MCPTool、ListMcpResourcesTool 等路径里带 mcp 的条目)。
边界 :这是 字符串启发式 ,不是协议层注册表;误杀/漏杀都可能在(例如路径改名不含 mcp 但实际是 MCP 工具)。成熟产品通常会改为 显式 kind / protocol 字段。
3.3 权限上下文:按工具名 deny
python
# 56:59:src/tools.py
def filter_tools_by_permission_context(tools: tuple[PortingModule, ...], permission_context: ToolPermissionContext | None = None) -> tuple[PortingModule, ...]:
if permission_context is None:
return tools
return tuple(module for module in tools if not permission_context.blocks(module.name))
python
# 18:20:src/permissions.py
def blocks(self, tool_name: str) -> bool:
lowered = tool_name.lower()
return lowered in self.deny_names or any(lowered.startswith(prefix) for prefix in self.deny_prefixes)
效果 :被 block 的工具 不出现在池子里 (能力面收缩)。这与 PermissionDenial 事件是两条线:前者是 「列表里就没有」 ,后者是 「模型点了但被拒」 (见 result/05.md)。
顺序 :先 simple → 再 MCP → 再 permission。若 simple 已只剩 3 个工具,MCP 与 deny 只在这 3 个上继续过滤。
4. CLI 如何把策略旋钮暴露给用户
tools 子命令把 与池子相同的参数 接到 get_tools:
python
# 40:46:src/main.py
tools_parser = subparsers.add_parser('tools', help='list mirrored tool entries from the archived snapshot')
tools_parser.add_argument('--limit', type=int, default=20)
tools_parser.add_argument('--query')
tools_parser.add_argument('--simple-mode', action='store_true')
tools_parser.add_argument('--no-mcp', action='store_true')
tools_parser.add_argument('--deny-tool', action='append', default=[])
tools_parser.add_argument('--deny-prefix', action='append', default=[])
python
# 132:140:src/main.py
if args.command == 'tools':
if args.query:
print(render_tool_index(limit=args.limit, query=args.query))
else:
permission_context = ToolPermissionContext.from_iterables(args.deny_tool, args.deny_prefix)
tools = get_tools(simple_mode=args.simple_mode, include_mcp=not args.no_mcp, permission_context=permission_context)
output_lines = [f'Tool entries: {len(tools)}', '']
output_lines.extend(f'- {module.name} --- {module.source_hint}' for module in tools[: args.limit])
print('\n'.join(output_lines))
return 0
注意 --query 分支 :走 render_tool_index → find_tools,它在 PORTED_TOOLS 上搜索 ,不应用 simple/MCP/permission 过滤。用途是「在全镜像里找关键词」,不是「当前池里找」。
测试 test_tool_permission_filtering_cli_runs 验证 --deny-prefix mcp 后列表中不再出现 MCPTool。
5. 关键边界:池子 不等于 路由全宇宙
PortRuntime.route_prompt 在 全量 PORTED_TOOLS 上匹配,而不是 get_tools() 过滤后的子集(见 result/08.md、result/09.md)。
因此:
- Tool Pool /
tools列表 描述的是 策略下的「展示给模型/用户的候选面」; - 路由 仍可能在 策略禁止 的工具名上命中------产品化时需要 在路由后二次过滤 或 只把池内名字暴露给模型。
6. system_init 与池策略的错位(阅读源码时要留意)
build_system_init_message 使用 无参 get_tools():
python
# 8:12:src/system_init.py
def build_system_init_message(trusted: bool = True) -> str:
setup = run_setup(trusted=trusted)
commands = get_commands()
tools = get_tools()
因此 bootstrap 报告里的 "Loaded tool entries" 计数 对应 默认最大池 ,不一定等于你刚用 --simple-mode 在 CLI 里看到的列表。若未来要在报告里体现「当前会话策略」,需要把 同一套 get_tools 参数 贯穿 bootstrap_session / system_init。
7. 小结表
| 开关 | 默认(tool-pool) |
对可用工具面的影响 |
|---|---|---|
simple_mode |
False |
True → 仅 3 个固定工具名 |
include_mcp |
True |
False → 去掉名字或路径含 mcp 的条目 |
permission_context |
None |
非空 → 按精确名与前缀拒绝,条目从池中消失 |
| Markdown 展示 | 前 15 条 | 不改变池大小,只影响输出长度 |
一句话 :Tool Pool 组装把 「可用工具面」变成可配置、可打印、可测试的函数 ;理解它的关键是 过滤顺序、启发式边界、以及与路由/查询路径是否共用同一过滤。