Solon v4.1.1

harness - 能力控制:Talent 启用禁用与工具权限

</> markdown
2026年10月8日 上午9:34:02

本文专门讲能力的开关控制:一个内置 Talent 要被模型看见、能被调用,到底要过哪几道闸门;toolsAdd(...) 与 disallowedToolsAdd(...) 的真实语义;以及哪些开关"即时生效"、哪些必须重建主代理。

相关文章:《配置参考》给出全部配置项的默认值;《工具名映射与权限解析参考》给出工具名到 Talent 的完整映射;《子代理定义与任务委派》讲定义级(front matter)的 tools / disallowedTools;《挂载机制详解》讲单技能与挂载级别的禁用。本文只讲引擎层的启用禁用与工具权限。

1、能力的四道闸门

一次请求里,某个工具能否出现,要依次通过四道闸门:

闸门判定位置判定内容
A 工具清单HarnessEngine.Builder.toolsAdd(...) / AgentDefinition 的 tools该工具名是否被声明
B 禁用名单AgentFactory.toolAddDo()是否被定义级或全局级 disallowedTools 命中
C Talent 启用位TalentUtil.activeTalents()承载它的 Talent isEnabled() 是否为 true
D 运行前置条件TalentUtil.activeTalents()isSupported(prompt) 是否成立(有无 provider / 服务器 / 可用工具)

前两道闸门发生在构建 Agent 时(决定工具是否进入注册表);后两道发生在每次请求时(决定这次是否注入工具与指令)。

// AgentFactory.create:先展开快捷权限,再逐个走 toolAddDo
for (String toolName : metadata.getTools()) {
    if ("**".equals(toolName)) { /* 展开 TOOL_ALL_FULL 后逐个 toolAddDo */ }
    else if ("*".equals(toolName)) { /* 展开 TOOL_ALL_PUBLIC 后逐个 toolAddDo */ }
    else if ("pi".equals(toolName)) { /* 展开 TOOL_PI 后逐个 toolAddDo */ }
    else { toolAddDo(engine, builder, terminalTalentProxy, metadata, toolName); }
}
// AgentFactory.toolAddDo:闸门 B —— 两个层级的禁用名单,命中即 return
if (metadata.getDisallowedTools().contains(toolName)) return;   // 定义级
if (engine.getDisallowedTools().contains(toolName)) return;     // 全局级

// switch 里对应到具体 Talent / 拦截器,例如:
case "task":     engine.getTaskTalent().setEnabled(engine.isSubagentEnabled());
                 builder.defaultTalentAdd(engine.getTaskTalent()); break;
case "hitl":     engine.getHitlInterceptor().setEnabled(engine.isHitlEnabled());
                 builder.defaultInterceptorAdd(engine.getHitlInterceptor()); break;
// TalentUtil.activeTalents:闸门 C / D —— 请求时过滤,跳过者既不注入指令也不部署工具
if (talent.isEnabled() == false || talent.isSupported(prompt) == false) {
    continue;
}

2、内置 Talent 与开关对照

能力工具名承载者关闭方式生效时机
文件与终端read write edit glob grep ls(list) bashTerminalTalentProxy → TerminalTalent从工具清单移除,或加入禁用名单工具权限变更需重建主代理
异步终端bash_start bash_wait bash_stop同上同上;或 bashAsyncEnabled(false)(默认 false)开关即时生效(getTools() 时过滤)
当前时间nowClockTalent只能 engine.getClockTalent().setEnabled(false)即时生效
任务清单todo(todoread todowrite)主代理走 TodoTalent(文件模式);子代理走 planningMode(true)(内存模式)从工具清单移除需重建
子代理委派task(subagent)TaskTalentsubagentEnabled(false),或从工具清单移除即时生效
动态子代理generateGenerateTalentsubagentEnabled(false),或从工具清单移除即时生效
心智记忆memoryMemoryTalentmemoryEnabled(false)(需已配置 memoryProvider)即时生效
人工介入hitlHITLInterceptor(拦截器,非 Talent)hitlEnabled(false),或工具清单不含 hitl即时生效(改的是拦截器实例)
专家技能skillSkillTalent工具名移除;单技能用 disallowSkill("@alias/path")单技能禁用即时生效
编码指引codeCodeTalent从工具清单移除需重建
网页/搜索webfetch websearch codesearchWebfetchTalent / WebsearchTalent / CodeSearchTalent从工具清单移除需重建
LSPlspLspTalent工具名移除;无服务器时不注入(isSupported = hasServers())有服务器才生效
MCPmcpMcpGatewayTalent工具名移除;服务级 McpServerParameters.isEnabled(false)无可用工具即不注入
OpenAPIopenapi(restapi)OpenApiGatewayTalent工具名移除;源级 ApiSource.isEnabled(false)无可用工具即不注入

几个默认值(与《配置参考》一致):memoryEnabled=true、subagentEnabled=true、sandboxEnabled=true、sandboxAllowUserHome=true、sandboxSystemRestrict=false、hitlEnabled=false、bashAsyncEnabled=false、multitaskMaxTasks=5。

3、toolsAdd 与 disallowedToolsAdd

两个方法各有三个重载,没有 String... 重载:

public Builder toolsAdd(ToolName... tools)
public Builder toolsAdd(Collection<String> tools)
@Deprecated public Builder toolsAdd(ToolPermission... tools)   // 4.0.4 起弃用

public Builder disallowedToolsAdd(ToolName... tools)
public Builder disallowedToolsAdd(Collection<String> tools)
@Deprecated public Builder disallowedToolsAdd(ToolPermission... tools)

语义差异(HarnessOptions):

  • ToolName... / ToolPermission...:只把枚举的 getName() 放进 Set,不做任何展开、不做校验;ToolName.TOOL_ALL_FULL 进去的就是字符串 "**",真正的展开发生在 AgentFactory.create。
  • Collection<String>:直接 addAll,连枚举归一都没有,写什么就是什么。
  • 需要字符串混写时用集合:
.toolsAdd(Arrays.asList("read", "glob", "grep", "ls"))
.disallowedToolsAdd(Arrays.asList("bash", "write", "edit"))

3.1 快捷权限的展开表

代号展开结果
piread write edit bash bash_start bash_wait bash_stop
*read write edit glob grep ls bash bash_start bash_wait bash_stop skill todo code codesearch websearch webfetch task lsp
*** 的全部 + generate mcp openapi hitl lsp memory(共 23 项)

展开是逐项调用 toolAddDo 的,所以展开出来的每一项仍会单独过禁用名单——这就是"全给但刨掉几个"的写法:

HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
        .sessionProvider(sessionProvider)
        .toolsAdd(ToolName.TOOL_ALL_FULL)                   // 先全给
        .disallowedToolsAdd(ToolName.TOOL_BASH,             // 再刨掉危险项
                            ToolName.TOOL_WRITE,
                            ToolName.TOOL_EDIT)
        .build();

4、静态开关与动态开关

HarnessEngine 上与之相关的方法,按"是否需要重建主代理"分两类(重建统一走 agentLock + createMainAgent()):

类别方法说明
需重建主代理allowTool(name) / disallowTool(name)同时维护白名单与黑名单:授权时从黑名单移除并加入白名单;禁用反之
需重建主代理allowToolReset(tools) / disallowToolReset(tools)只重置一侧
需重建主代理toolPermissionReset(tools, disallowedTools)一次性重置两侧,只重建一次(避免连续两次重置造成双倍抖动)
需重建主代理refreshMainAgent()手工兜底:置空后下次 getMainAgent() 懒加载重建
需重建主代理addExtension / removeExtension / addLspServer / removeLspServer扩展与 LSP 服务器会改变工具集
即时生效setMemoryEnabled / setSubagentEnabled / setBashAsyncEnabled / setHitlEnabled直接改 Talent / 拦截器实例的 enabled 位
即时生效setSandboxEnabled / setSandboxAllowUserHome / setSandboxSystemRestrict改 TerminalTalent 行为,不增减工具
即时生效setMultitaskMaxTasks / setCompressionThreshold / setMemoryRelevanceCount / setMemoryPriorityCount / setMemorySummaryLength参数类
即时生效disallowSkill(aliasPath) / allowSkill(aliasPath) / disallowSkillReset(...)单技能粒度
// 运行时收紧:把 bash 从白名单挪进黑名单,并重建主代理使其立即生效
engine.disallowTool("bash");

// 运行时放宽:重新授权
engine.allowTool("bash");

// 一次性重置两侧(只重建一次)
engine.toolPermissionReset(
        Arrays.asList("read", "write", "edit", "glob", "grep", "ls"),
        Arrays.asList("bash"));

子代理是按需构建的(engine.getAgentOrMain(name) 每次都走 AgentFactory.create(...).build()),因此上述动态改动对之后构建的子代理同样生效。

5、容易踩的细节

  1. 禁用名单只认"具体工具名",不认快捷代号。禁用判断发生在展开之后,disallowedToolsAdd("pi")(或 "*"、"**")永远不会命中任何一项,等于没写。
  2. 别名与规范名不互通。禁用判断比较的是原始字符串,case 归一只发生在判断之后:toolsAdd("list") + disallowedToolsAdd("ls") 拦不住 list。两侧请用同一写法。
  3. edit 的附带项会被单独拦截。case "edit" 会递归补 read 和 write,这两个子项各自过一次禁用名单——所以可以出现"edit 在、write 被刨掉"的组合。
  4. now 不受工具清单与禁用名单控制。只要工具清单非空(哪怕全部被禁用),AgentFactory 都会追加 ClockTalent,且这一步不查禁用名单。想去掉只能 engine.getClockTalent().setEnabled(false)。
  5. task / generate / hitl 的 enabled 位是"构建期同步"的。toolAddDo 里会用引擎当前值覆盖 Talent / 拦截器状态,所以在 Builder 上重复设置无意义,运行时请用 setSubagentEnabled / setHitlEnabled。
  6. memory 需要 memoryProvider。未配置时 engine.getMemoryTalent() 返回 null,把 memory 写进工具清单会在构建代理时因 talentAdd 取 t.getName() 抛 NPE;此时调用 setMemoryEnabled(...) 同样会 NPE(该方法没有 null 守卫)。先配 provider(或不要声明 memory)。
  7. "删工具名"和"关 Talent"不是一回事。前者让工具不进入注册表,但必须重建主代理才生效;后者工具名仍在,只是请求时被 TalentUtil 跳过(既不注入指令也不部署工具),是即时生效的软开关。
  8. 沙盒三开关不减少工具。它们只改 TerminalTalent 的执行行为(绝对路径受限等),read/bash 依然存在。
  9. 定义级与全局级优先级相同。两者都是"命中即 return",没有谁覆盖谁;全局名单影响所有代理(含子代理),定义级只影响自己。
  10. MCP / OpenAPI 是两层开关。mcp / openapi 工具名决定"这一族是否注册",服务级 McpServerParameters.isEnabled / ApiSource.isEnabled 决定"这个服务是否接入",isSupported() 再看是否有可用工具。

6、场景配方

只读助手(只给看,不给改):

HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
        .sessionProvider(sessionProvider)
        .toolsAdd(Arrays.asList("read", "glob", "grep", "ls"))
        .build();

全给但收口危险项:

HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
        .sessionProvider(sessionProvider)
        .toolsAdd(ToolName.TOOL_ALL_FULL)
        .disallowedToolsAdd(ToolName.TOOL_BASH, ToolName.TOOL_WRITE, ToolName.TOOL_EDIT)
        .bashAsyncEnabled(false)
        .build();

关掉子代理与记忆(省 token,不重建):

engine.setSubagentEnabled(false);   // task / generate 不再注入
engine.setMemoryEnabled(false);     // 需已配置 memoryProvider

保留全部工具但禁掉某几个技能:

engine.disallowSkill("@my-skills/expensive-analysis");