Skip to content

fix(harness): make allowed_tool_names distinguish None from All #583

Description

@zhnt

问题

allowed_tool_names 用一个 None 同时表达两种语义完全不同的状态:

# src/loushang/harness/capabilities/tools.py:134
allowed_names: Iterable[str] | None = None,
# :141-144
self._allowed_names = (
    frozenset(_unique_names(allowed_names)) if allowed_names is not None else None
)
# :314
def is_allowed(self, name: str) -> bool:
    return self._allowed_names is None or name in self._allowed_names   # ← None 恒为 True

实测三种状态(ToolActivationCoordinator,available=read/bash/edit):

allowed_names is_allowed("read") is_allowed("bash") active
None True True ('read','bash','edit')
[] False False ()
["read"] True False ('read',)

None = 无上限(fail-open),[] = 无工具,两者语义相反但都是"缺省/空"的直觉候选。

为什么这不是风格问题

None 是 AgentProductSession.__init__ 的参数缺省值(harness/session/agent_product.py:370):

allowed_tool_names: list[str] | None = None,

省略该参数 = 无上限。 调用方不会报错,只会静默 fail-open。这与本仓库自身的既定原则冲突:

  • tool-governance.md:392:Model Call preparation 超时/失败时返回 default_selection_pending 并 fails preparation closed;
  • capability-runtime-convergence-pr0-baseline.md:94(COMP-03):ProductRuntimeBindings.register_tool 的默认绑定构造兼容,但试图实际注册工具时 fail closed。

即:仓库在别处明确选择 fail-closed,此处却 fail-open,且缺省值静默生效。

服务端场景下的实际影响

多用户/多租户托管中,"装配时忘记传工具上限"是一个现实的编码错误。后果是该会话获得工具目录里的全部工具,且没有任何报错或诊断。这与 allowed_tool_names 在服务端承担的角色(唯一能在装配期收窄工具面的手段)直接矛盾。

需要注意的是:这不是唯一防线,也不是"越权"——Invocation Policy 仍然逐次求值。但它确实把"装配期上限"这一层静默移除了。

目标态已经解决了一部分

tool-governance.md 的 governed-v1 模型已经区分了这两种状态,没有 None 歧义:

IntentSelectionMode.INHERIT_DEFAULTS    # 默认继承 + 显式增删
IntentSelectionMode.EXPLICIT_ONLY       # 只有显式启用项,可为空

set_explicit_only(names, expected_revision) 明确支持"空选择"(tool-governance.md:655 "including an empty no-tools selection")。

所以本 issue 的性质是:legacy 路径的缺省值与目标态语义不一致,而 P1B cutover 尚未完成(tool-governance.md Status: "existing Product sessions remain on isolated legacy_positive state pending the P1B atomic control-surface cutover",tracking #517)。

建议

A. 若纳入 P1B(推荐):cutover 时对 None 采用 fail-closed 缺省,并显式提供"无上限"入口(如 IntentSelectionMode 或 ToolSelection.ALL 具名值)。迁移期可先加构造期诊断。

B. 若需在 P1B 前独立修复:把 None 拆成可区分的三态:

ToolSelection = Literal["all"] | frozenset[str]   # 或专用类型
# 缺省改为 frozenset()(无工具),"all" 必须显式书写

迁移注意:全仓 allowed_tool_names=None 的调用点较多(含测试),且 Coding CLI 依赖 None 表示"不限制"(coding/cli/__main__.py:232-238)。需同步审计所有调用点,并在 AgentProductSession / SessionToolRuntime / bootstrap_construction 三处保持一致。

验收要点

  • None 不再静默等价于"无上限"
  • "无上限"必须显式、可读、可 grep
  • 省略参数时 fail-closed,且有明确诊断
  • 现有 allowed_tool_names=None 调用点逐一审计并分类(真"无上限" vs 遗漏)
  • 三处装配路径(agent_product / tool_runtime / bootstrap_construction)语义一致

相关

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions