feat(policy,komodo,config): vault zone and firefly skill rules, komodo python_tool with exec, tests
This commit is contained in:
@@ -4,7 +4,6 @@ from datetime import UTC, date, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import aiohttp
|
||||
from beaver_gateway.agents.base import BaseAgent, ExposedMcp
|
||||
from beaver_gateway.agents.claude import ClaudeAgent, ClaudeOptions, Prompts
|
||||
from beaver_gateway.agents.raycast import RaycastAgent, RemoteTool, UserPreferences
|
||||
@@ -25,6 +24,19 @@ from beaver_gateway.frontends.mcp_server import McpServerFrontend
|
||||
from beaver_gateway.frontends.telegram import TelegramFrontend
|
||||
from beaver_gateway.mcp.types import HttpMcp, McpServer, McpServerT
|
||||
|
||||
from mcps.komodo import Komodo
|
||||
from policy import (
|
||||
DEEP_DISALLOWED,
|
||||
DISPATCHER_DISALLOWED,
|
||||
DISTILLER_DISALLOWED,
|
||||
TRIAGE_DISALLOWED,
|
||||
Zones,
|
||||
bash_zones,
|
||||
requires_skill,
|
||||
skill_tracker,
|
||||
vault_zones,
|
||||
)
|
||||
|
||||
TZ = "Europe/Warsaw"
|
||||
VAULT = Path("/vault")
|
||||
CHATS_DIR = VAULT / "💬 чаты"
|
||||
@@ -99,6 +111,18 @@ DEEP_SKILLS = (SKILLS / "общие", SKILLS / "vault")
|
||||
DISPATCHER_SKILLS = (SKILLS / "общие", SKILLS / "диспетчер", SKILLS / "vault")
|
||||
|
||||
|
||||
# §3.7: зоны vault - запись только в мета/бобер, в 💬 чаты только новые файлы;
|
||||
# firefly пишет только после открытого скилла. Маунт ro - первая линия.
|
||||
ZONES = Zones(vault=VAULT, write=(BEAVER,), create=(CHATS_DIR,))
|
||||
FIREFLY_WRITES = ("mcp__firefly__store_*", "mcp__firefly__update_*")
|
||||
VAULT_POLICY = (
|
||||
skill_tracker(),
|
||||
vault_zones(ZONES),
|
||||
bash_zones(ZONES),
|
||||
requires_skill("firefly", FIREFLY_WRITES),
|
||||
)
|
||||
|
||||
|
||||
def chat_path(title: str, agent: str, vault: Path) -> Path: # noqa: ARG001
|
||||
"""Новый файл чата в `💬 чаты/`: помесячная папка, дата и тема."""
|
||||
today = date.today()
|
||||
@@ -125,6 +149,21 @@ def _calendar_mcps() -> list[HttpMcp]:
|
||||
calendar_mcps = _calendar_mcps()
|
||||
calendar_exposed = tuple(ExposedMcp(name=m.name) for m in calendar_mcps)
|
||||
|
||||
# §4.4: komodo - python_tool с перечислимыми действиями, ключ остаётся в gateway.
|
||||
KOMODO = (
|
||||
Komodo(
|
||||
url=os.environ["KOMODO_URL"],
|
||||
key=os.environ["KOMODO_KEY"],
|
||||
secret=os.environ["KOMODO_SECRET"],
|
||||
)
|
||||
if os.environ.get("KOMODO_URL")
|
||||
else None
|
||||
)
|
||||
komodo_mcps = (
|
||||
[McpServer.python_tool(name="komodo", tools=[KOMODO.komodo])] if KOMODO else []
|
||||
)
|
||||
komodo_exposed = (ExposedMcp(name="komodo"),) if KOMODO else ()
|
||||
|
||||
# Секреты MCP - только через env подпроцесса (mcp stdio даёт ему белый список
|
||||
# + это), никогда argv: процесс модели видит `ps` всего контейнера.
|
||||
mcps: list[McpServerT] = [
|
||||
@@ -144,6 +183,7 @@ mcps: list[McpServerT] = [
|
||||
),
|
||||
McpServer.http(name="telegram", url=os.environ["BEAVERGRAM_MCP"]),
|
||||
*calendar_mcps,
|
||||
*komodo_mcps,
|
||||
]
|
||||
|
||||
# Руки глубокого (§4.4): firefly без delete_*, obsidian-fs не даётся - свои
|
||||
@@ -152,6 +192,7 @@ CLAUDE_MCPS = (
|
||||
ExposedMcp(name="firefly", deny=("delete_*",)),
|
||||
ExposedMcp(name="telegram"),
|
||||
*calendar_exposed,
|
||||
*komodo_exposed,
|
||||
)
|
||||
|
||||
|
||||
@@ -168,11 +209,8 @@ def dispatcher(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
|
||||
prompts=DISPATCHER_PROMPTS,
|
||||
skill_sets=DISPATCHER_SKILLS,
|
||||
gateway_tools=("read_conversation", "spawn", "say", "schedule"),
|
||||
options=ClaudeOptions(
|
||||
effort=effort,
|
||||
# §3.7: у диспетчера AskUserQuestion остаётся, планов и ноутбуков нет.
|
||||
disallowed_tools=("ExitPlanMode", "EnterPlanMode", "NotebookEdit"),
|
||||
),
|
||||
options=ClaudeOptions(effort=effort, disallowed_tools=DISPATCHER_DISALLOWED),
|
||||
policy=VAULT_POLICY,
|
||||
expose_mcps=CLAUDE_MCPS,
|
||||
)
|
||||
|
||||
@@ -186,17 +224,8 @@ def deep(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
|
||||
skill_sets=DEEP_SKILLS,
|
||||
# §8.4: «ок, обсудили» - тулза, закрытие после ответа.
|
||||
gateway_tools=("close_chat",),
|
||||
options=ClaudeOptions(
|
||||
effort=effort,
|
||||
# §3.7: у глубоких вопросы текстом, планов и сабагентов нет.
|
||||
disallowed_tools=(
|
||||
"AskUserQuestion",
|
||||
"ExitPlanMode",
|
||||
"EnterPlanMode",
|
||||
"NotebookEdit",
|
||||
"Task",
|
||||
),
|
||||
),
|
||||
options=ClaudeOptions(effort=effort, disallowed_tools=DEEP_DISALLOWED),
|
||||
policy=VAULT_POLICY,
|
||||
expose_mcps=CLAUDE_MCPS,
|
||||
)
|
||||
|
||||
@@ -211,8 +240,9 @@ def distiller(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
|
||||
options=ClaudeOptions(
|
||||
effort=effort,
|
||||
tools=("Read", "Write"),
|
||||
disallowed_tools=("AskUserQuestion", "Task", "WebSearch", "WebFetch"),
|
||||
disallowed_tools=DISTILLER_DISALLOWED,
|
||||
),
|
||||
policy=(vault_zones(ZONES),),
|
||||
)
|
||||
|
||||
|
||||
@@ -225,9 +255,7 @@ def triage(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
|
||||
prompts=TRIAGE_PROMPTS,
|
||||
gateway_tools=("say", "inject"),
|
||||
options=ClaudeOptions(
|
||||
effort=effort,
|
||||
tools=(),
|
||||
disallowed_tools=("AskUserQuestion", "Task", "WebSearch", "WebFetch"),
|
||||
effort=effort, tools=(), disallowed_tools=TRIAGE_DISALLOWED
|
||||
),
|
||||
)
|
||||
|
||||
@@ -451,26 +479,13 @@ KOMODO_STACK = os.environ.get("KOMODO_STACK", "beaver-agent")
|
||||
|
||||
|
||||
async def komodo_deploy() -> None:
|
||||
url, key, secret = (
|
||||
os.environ.get(k, "") for k in ("KOMODO_URL", "KOMODO_KEY", "KOMODO_SECRET")
|
||||
)
|
||||
if not (url and key and secret):
|
||||
if KOMODO is None:
|
||||
_log.warning("deploy hook: KOMODO_URL/KEY/SECRET are not set, nothing deployed")
|
||||
return
|
||||
body = {"type": "DeployStack", "params": {"stack": KOMODO_STACK, "services": []}}
|
||||
headers = {"X-Api-Key": key, "X-Api-Secret": secret}
|
||||
async with (
|
||||
aiohttp.ClientSession() as http,
|
||||
http.post(
|
||||
f"{url.rstrip('/')}/execute",
|
||||
json=body,
|
||||
headers=headers,
|
||||
timeout=aiohttp.ClientTimeout(total=30),
|
||||
) as response,
|
||||
):
|
||||
_log.info(
|
||||
"deploy hook: komodo %s %s", response.status, (await response.text())[:200]
|
||||
)
|
||||
receipt = await KOMODO.execute(
|
||||
"DeployStack", {"stack": KOMODO_STACK, "services": []}
|
||||
)
|
||||
_log.info("deploy hook: komodo accepted %s", receipt.get("id"))
|
||||
|
||||
|
||||
async def deploy(run: JobRun) -> None:
|
||||
|
||||
Reference in New Issue
Block a user