mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-04 07:20:10 +00:00
Imported from andrewyng/aisuite@1b4bbf303e (contents of its platform/ directory, hoisted to the repo root). Development history prior to this commit lives in that repository. Co-authored-by: Devika <devikaverma11@gmail.com>
181 lines
5.9 KiB
Python
181 lines
5.9 KiB
Python
"""Vetted tool catalog — the stable ``id → capability`` layer a persona references.
|
|
|
|
A *capability* bundles a group of tools (the existing ``tools/`` factories) behind a stable
|
|
id, plus what session context it needs (``requires``) and the risk classes it can produce
|
|
(``risk``, used by the Phase 2 install-consent screen). ``expand(ids, context)`` turns a
|
|
persona's ``tools:`` list into concrete callables, skipping capabilities whose context
|
|
prerequisites aren't met (e.g. no shell without an executor) — matching the per-agent
|
|
factories that used to assemble tools by hand.
|
|
|
|
The catalog is **platform-owned and closed**: third parties get breadth from us adding
|
|
vetted capabilities here and from MCP, never by adding entries. MCP tools are *not* in the
|
|
catalog (see ``PERMISSIONS-AND-INBOX.md``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Callable
|
|
|
|
import aisuite as ai
|
|
|
|
from .agents.base import AgentContext
|
|
from .risk import RiskClass
|
|
from .tools.files import file_tools
|
|
from .tools.git import git_tools
|
|
from .tools.search import search_tools
|
|
from .tools.shell import shell_tools
|
|
from .tools.todo import todo_tools
|
|
|
|
# Context prerequisites a capability may require, mapped to a predicate over AgentContext.
|
|
_REQUIREMENTS: dict[str, Callable[[AgentContext], bool]] = {
|
|
"workspace": lambda c: c.workspace is not None,
|
|
"executor": lambda c: c.executor is not None,
|
|
"todo": lambda c: c.todo is not None,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Capability:
|
|
id: str
|
|
name: str # human label (consent screen)
|
|
description: str
|
|
build: Callable[[AgentContext], list]
|
|
requires: tuple[str, ...] = ()
|
|
risk: tuple[RiskClass, ...] = (RiskClass.READ,)
|
|
|
|
def available(self, context: AgentContext) -> bool:
|
|
return all(_REQUIREMENTS[r](context) for r in self.requires)
|
|
|
|
|
|
# -- capability builders --------------------------------------------------------
|
|
# These reproduce, exactly, what the Code and Cowork agent factories assembled by hand.
|
|
|
|
|
|
def _code_files(context: AgentContext) -> list:
|
|
"""Repo-oriented files: single-root, line-numbered/windowed `read_file`. Our `grep` and
|
|
windowed `read_file` replace aisuite's slower `search_files` / `read_file`/`read_file_lines`.
|
|
"""
|
|
ws = str(context.workspace)
|
|
replaced = {"search_files", "read_file", "read_file_lines"}
|
|
files = [
|
|
t
|
|
for t in ai.toolkits.files(root=ws, allow_write=True)
|
|
if getattr(t, "__name__", "") not in replaced
|
|
]
|
|
return [*files, *file_tools(ws)]
|
|
|
|
|
|
def _files(context: AgentContext) -> list:
|
|
"""Knowledge-work files: multi-root aware (reads/writes across the session's roots), keeps
|
|
aisuite's `read_file`/`read_file_lines`. Only our `grep` replaces the slow `search_files`.
|
|
"""
|
|
ws = str(context.workspace)
|
|
file_kwargs = (
|
|
{"roots": context.roots} if context.roots else {"root": ws, "allow_write": True}
|
|
)
|
|
return [
|
|
t
|
|
for t in ai.toolkits.files(**file_kwargs)
|
|
if getattr(t, "__name__", "") != "search_files"
|
|
]
|
|
|
|
|
|
def _git(context: AgentContext) -> list:
|
|
ws = str(context.workspace)
|
|
return [*ai.toolkits.git(root=ws), *git_tools(ws)] # git_status, git_diff, git_log
|
|
|
|
|
|
def _search(context: AgentContext) -> list:
|
|
return search_tools(str(context.workspace)) # grep (ripgrep, .gitignore-aware)
|
|
|
|
|
|
def _shell(context: AgentContext) -> list:
|
|
return shell_tools(context.executor) # run_shell + background task tools
|
|
|
|
|
|
def _todo(context: AgentContext) -> list:
|
|
return todo_tools(context.todo) # todo_write (drives the Progress panel)
|
|
|
|
|
|
_CAPS: list[Capability] = [
|
|
Capability(
|
|
id="code_files",
|
|
name="Code files",
|
|
description="Read & edit files in a single repo workspace (line-numbered reads).",
|
|
build=_code_files,
|
|
requires=("workspace",),
|
|
risk=(RiskClass.READ, RiskClass.WRITE_LOCAL),
|
|
),
|
|
Capability(
|
|
id="files",
|
|
name="Files",
|
|
description="Read & edit files across the session's workspace folders.",
|
|
build=_files,
|
|
requires=("workspace",),
|
|
risk=(RiskClass.READ, RiskClass.WRITE_LOCAL),
|
|
),
|
|
Capability(
|
|
id="git",
|
|
name="Git",
|
|
description="Inspect git state and history (status, diff, log).",
|
|
build=_git,
|
|
requires=("workspace",),
|
|
risk=(RiskClass.READ,),
|
|
),
|
|
Capability(
|
|
id="search",
|
|
name="Search",
|
|
description="Fast code/content search (grep).",
|
|
build=_search,
|
|
requires=("workspace",),
|
|
risk=(RiskClass.READ,),
|
|
),
|
|
Capability(
|
|
id="shell",
|
|
name="Shell",
|
|
description="Run shell commands in a persistent session.",
|
|
build=_shell,
|
|
requires=("executor",),
|
|
risk=(RiskClass.EXEC,),
|
|
),
|
|
Capability(
|
|
id="todo",
|
|
name="Task list",
|
|
description="Maintain a visible task/progress list.",
|
|
build=_todo,
|
|
requires=("todo",),
|
|
risk=(RiskClass.READ,),
|
|
),
|
|
]
|
|
|
|
CATALOG: dict[str, Capability] = {c.id: c for c in _CAPS}
|
|
|
|
|
|
def capability(cap_id: str) -> Capability:
|
|
cap = CATALOG.get(cap_id)
|
|
if cap is None:
|
|
raise KeyError(f"Unknown capability id: {cap_id!r}")
|
|
return cap
|
|
|
|
|
|
def expand(ids: list[str], context: AgentContext) -> list:
|
|
"""Expand a persona's ``tools:`` id list into concrete tool callables for this context.
|
|
Capabilities whose context prerequisites aren't met are skipped (no shell without an
|
|
executor, no files without a workspace) — exactly like the old hand-written factories.
|
|
"""
|
|
tools: list = []
|
|
for cap_id in ids:
|
|
cap = capability(cap_id)
|
|
if cap.available(context):
|
|
tools.extend(cap.build(context))
|
|
return tools
|
|
|
|
|
|
def risk_summary(ids: list[str]) -> set[RiskClass]:
|
|
"""The union of risk classes a tool list can produce — for the install-consent screen."""
|
|
out: set[RiskClass] = set()
|
|
for cap_id in ids:
|
|
out.update(capability(cap_id).risk)
|
|
return out
|