harness - 能力控制:Talent 启用禁用与工具权限
本文专门讲能力的开关控制:一个内置 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) bash | TerminalTalentProxy → TerminalTalent | 从工具清单移除,或加入禁用名单 | 工具权限变更需重建主代理 |
| 异步终端 | bash_start bash_wait bash_stop | 同上 | 同上;或 bashAsyncEnabled(false)(默认 false) | 开关即时生效(getTools() 时过滤) |
| 当前时间 | now | ClockTalent | 只能 engine.getClockTalent().setEnabled(false) | 即时生效 |
| 任务清单 | todo(todoread todowrite) | 主代理走 TodoTalent(文件模式);子代理走 planningMode(true)(内存模式) | 从工具清单移除 | 需重建 |
| 子代理委派 | task(subagent) | TaskTalent | subagentEnabled(false),或从工具清单移除 | 即时生效 |
| 动态子代理 | generate | GenerateTalent | subagentEnabled(false),或从工具清单移除 | 即时生效 |
| 心智记忆 | memory | MemoryTalent | memoryEnabled(false)(需已配置 memoryProvider) | 即时生效 |
| 人工介入 | hitl | HITLInterceptor(拦截器,非 Talent) | hitlEnabled(false),或工具清单不含 hitl | 即时生效(改的是拦截器实例) |
| 专家技能 | skill | SkillTalent | 工具名移除;单技能用 disallowSkill("@alias/path") | 单技能禁用即时生效 |
| 编码指引 | code | CodeTalent | 从工具清单移除 | 需重建 |
| 网页/搜索 | webfetch websearch codesearch | WebfetchTalent / WebsearchTalent / CodeSearchTalent | 从工具清单移除 | 需重建 |
| LSP | lsp | LspTalent | 工具名移除;无服务器时不注入(isSupported = hasServers()) | 有服务器才生效 |
| MCP | mcp | McpGatewayTalent | 工具名移除;服务级 McpServerParameters.isEnabled(false) | 无可用工具即不注入 |
| OpenAPI | openapi(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 快捷权限的展开表
| 代号 | 展开结果 |
|---|---|
pi | read 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、容易踩的细节
- 禁用名单只认"具体工具名",不认快捷代号。禁用判断发生在展开之后,
disallowedToolsAdd("pi")(或"*"、"**")永远不会命中任何一项,等于没写。 - 别名与规范名不互通。禁用判断比较的是原始字符串,
case归一只发生在判断之后:toolsAdd("list")+disallowedToolsAdd("ls")拦不住list。两侧请用同一写法。 edit的附带项会被单独拦截。case "edit"会递归补read和write,这两个子项各自过一次禁用名单——所以可以出现"edit在、write被刨掉"的组合。now不受工具清单与禁用名单控制。只要工具清单非空(哪怕全部被禁用),AgentFactory都会追加ClockTalent,且这一步不查禁用名单。想去掉只能engine.getClockTalent().setEnabled(false)。task/generate/hitl的 enabled 位是"构建期同步"的。toolAddDo里会用引擎当前值覆盖 Talent / 拦截器状态,所以在 Builder 上重复设置无意义,运行时请用setSubagentEnabled/setHitlEnabled。memory需要memoryProvider。未配置时engine.getMemoryTalent()返回null,把memory写进工具清单会在构建代理时因talentAdd取t.getName()抛 NPE;此时调用setMemoryEnabled(...)同样会 NPE(该方法没有 null 守卫)。先配 provider(或不要声明memory)。- "删工具名"和"关 Talent"不是一回事。前者让工具不进入注册表,但必须重建主代理才生效;后者工具名仍在,只是请求时被
TalentUtil跳过(既不注入指令也不部署工具),是即时生效的软开关。 - 沙盒三开关不减少工具。它们只改
TerminalTalent的执行行为(绝对路径受限等),read/bash依然存在。 - 定义级与全局级优先级相同。两者都是"命中即 return",没有谁覆盖谁;全局名单影响所有代理(含子代理),定义级只影响自己。
- 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");