项目文件夹

文件
Devika 410fc71a60 Phase 0: tool catalog + risk-class permissions
Foundation for personas and the permission model, with no behavior change for
existing agents.

- risk.py: RiskClass (read/write_local/exec/external) + classify(), with a
  user-local override seam for Phase 2. permissions.py now reads the risk class
  instead of the hardcoded WRITE_TOOLS/SHELL_TOOL sets (re-exported for
  back-compat); adds a risk_overrides field.
- catalog.py: the vetted id->capability layer. Capabilities wrap the existing
  tools/ factories with `requires` (context prerequisites) and `risk`. expand()
  turns a tools: id list into callables, skipping capabilities whose context is
  absent (no shell without an executor).
- Code and Cowork build via expand() now; their hand-written factories are gone.
  Two file capabilities preserve each surface's exact toolset: code_files
  (single-root, numbered reader) and files (multi-root, keeps read_file_lines).

Tests: test_catalog.py + test_permissions_risk.py (41 passed). Full suite 348
passed; the 3 failures are pre-existing anthropic/google SDK import errors in the
borrowed venv, unrelated to this change.
2026-06-27 06:47:14 +05:30

116 行
4.0 KiB
Python

"""Phase 0 gate — the vetted tool catalog.
Asserts capabilities register, ``expand`` reproduces the Code and Cowork toolsets exactly
(the equivalence net for the build_tools refactor), context prerequisites are honored
(no shell without an executor, no files without a workspace), and the Code/Cowork file-tool
distinction (single-root numbered reader vs multi-root) is preserved."""
from __future__ import annotations
import pytest
from coworker.agents.base import AgentContext
from coworker.agents.code import CODE_CAPABILITIES, code_agent
from coworker.agents.cowork import COWORK_CAPABILITIES, cowork_agent
from coworker.catalog import CATALOG, capability, expand, risk_summary
from coworker.risk import RiskClass
from coworker.tools.todo import TodoList
# Expected toolset for each surface — the frozen equivalence contract for the refactor.
CODE_TOOLS = {
"list_files",
"write_file",
"apply_unified_diff",
"apply_patch",
"replace_in_file",
"read_file", # numbered/windowed (single-root)
"git_status",
"git_diff",
"git_log",
"grep",
"run_shell",
"shell_task_output",
"shell_task_kill",
"todo_write",
}
COWORK_TOOLS = {
"list_files",
"read_file", # aisuite (multi-root)
"read_file_lines",
"write_file",
"apply_unified_diff",
"apply_patch",
"replace_in_file",
"grep",
"run_shell",
"shell_task_output",
"shell_task_kill",
"todo_write",
}
def _names(tools) -> set:
return {getattr(t, "__name__", "") for t in tools}
def _full_context(tmp_path) -> AgentContext:
return AgentContext(workspace=tmp_path, executor=object(), todo=TodoList())
def test_catalog_registers_expected_ids():
assert {"code_files", "files", "git", "search", "shell", "todo"} <= set(CATALOG)
for cap in CATALOG.values():
assert cap.id and cap.name and callable(cap.build)
def test_expand_code_matches_expected(tmp_path):
tools = expand(CODE_CAPABILITIES, _full_context(tmp_path))
assert _names(tools) == CODE_TOOLS
def test_expand_cowork_matches_expected(tmp_path):
tools = expand(COWORK_CAPABILITIES, _full_context(tmp_path))
assert _names(tools) == COWORK_TOOLS
def test_agents_use_catalog(tmp_path):
# The agent factories build through the catalog now — same result as direct expand.
ctx = _full_context(tmp_path)
assert _names(code_agent().build_tools(ctx)) == CODE_TOOLS
assert _names(cowork_agent().build_tools(ctx)) == COWORK_TOOLS
def test_file_capability_distinction(tmp_path):
# Code drops read_file_lines (folded into the windowed reader); Cowork keeps it (multi-root).
code = _names(expand(["code_files"], _full_context(tmp_path)))
cowork = _names(expand(["files"], _full_context(tmp_path)))
assert "read_file_lines" not in code
assert "read_file_lines" in cowork
assert "read_file" in code and "read_file" in cowork
def test_requirements_skip_unavailable(tmp_path):
# No executor → no shell; no todo → no todo_write; no workspace → no files/git/search.
no_exec = AgentContext(workspace=tmp_path, executor=None, todo=TodoList())
assert "run_shell" not in _names(expand(CODE_CAPABILITIES, no_exec))
assert "todo_write" in _names(expand(CODE_CAPABILITIES, no_exec))
no_todo = AgentContext(workspace=tmp_path, executor=object(), todo=None)
assert "todo_write" not in _names(expand(CODE_CAPABILITIES, no_todo))
assert "run_shell" in _names(expand(CODE_CAPABILITIES, no_todo))
no_ws = AgentContext(workspace=None, executor=object(), todo=TodoList())
names = _names(expand(CODE_CAPABILITIES, no_ws))
assert names == {"run_shell", "shell_task_output", "shell_task_kill", "todo_write"}
def test_risk_summary():
assert risk_summary(["shell"]) == {RiskClass.EXEC}
assert risk_summary(["code_files"]) == {RiskClass.READ, RiskClass.WRITE_LOCAL}
assert risk_summary(["git", "search"]) == {RiskClass.READ}
def test_unknown_capability_raises():
with pytest.raises(KeyError):
capability("does_not_exist")