feat(config,policy): skill sets per kind, master without vault skills, attachments in the vault, crons in local tz

This commit is contained in:
hh
2026-09-01 23:13:29 +02:00
parent 43d0d43f9d
commit f7844a5411
4 changed files with 89 additions and 20 deletions
+30 -7
View File
@@ -6,7 +6,7 @@ from typing import Any
from zoneinfo import ZoneInfo
from beaver_gateway.agents.base import BaseAgent, ExposedMcp
from beaver_gateway.agents.claude import ClaudeAgent, ClaudeOptions, Prompts
from beaver_gateway.agents.claude import ClaudeAgent, ClaudeOptions, Prompts, SkillSets
from beaver_gateway.agents.raycast import RaycastAgent, RemoteTool, UserPreferences
from beaver_gateway.core.conversations import (
ConversationTexts,
@@ -27,7 +27,7 @@ from beaver_gateway.frontends.api import ApiFrontend
from beaver_gateway.frontends.base import Frontend
from beaver_gateway.frontends.markdown import MarkdownFrontend
from beaver_gateway.frontends.mcp_server import McpServerFrontend
from beaver_gateway.frontends.telegram import TelegramFrontend
from beaver_gateway.frontends.telegram import Attachments, TelegramFrontend
from beaver_gateway.mcp.types import HttpMcp, McpServer, McpServerT
from curator import Watched, briefing
@@ -42,6 +42,7 @@ from policy import (
TRIAGE_DISALLOWED,
Zones,
bash_zones,
forbid_skills,
requires_skill,
skill_tracker,
vault_zones,
@@ -127,9 +128,23 @@ CURATOR_PROMPTS = Prompts(
)
QUICK_PROMPT = assemble((*VOICE, PROFILE, overlay("quick", "быстрый")))
# §4.3: наборы скиллов = папки = плагины. Глубокие грузят общие + vault.
# §4.3: наборы скиллов = папки = плагины. Глубокие грузят общие + vault;
# мастер - без vault (firefly, beaver durden - только в ветке и в глубоком);
# форк и джоб диспетчера - без скиллов вовсе.
DEEP_SKILLS = (SKILLS / "общие", SKILLS / "vault")
DISPATCHER_SKILLS = (SKILLS / "общие", SKILLS / "диспетчер", SKILLS / "vault")
DISPATCHER_SKILLS = SkillSets(
master=(SKILLS / "общие", SKILLS / "диспетчер"),
branch=(SKILLS / "общие", SKILLS / "диспетчер", SKILLS / "vault"),
fork=(),
job=(),
)
# Мастер не становится Дерденом: без набора скилл и так не найдётся, правило -
# ради внятного отказа («открой ветку»).
MASTER_FORBIDDEN_SKILLS = (
tuple(p.name for p in sorted((SKILLS / "vault").iterdir()) if p.is_dir())
if (SKILLS / "vault").exists()
else ()
)
# §3.7 (решение h, 2026-08-30): vault - дом диспетчера. Создавать, править,
@@ -290,7 +305,10 @@ def dispatcher(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
skill_sets=DISPATCHER_SKILLS,
gateway_tools=("read_conversation", "spawn", "say", "schedule"),
options=ClaudeOptions(effort=effort, disallowed_tools=DISPATCHER_DISALLOWED),
policy=VAULT_POLICY,
policy=(
*VAULT_POLICY,
forbid_skills(MASTER_FORBIDDEN_SKILLS, kinds=("master",)),
),
# §4.4: t3code и вайбграм - руки диспетчера (мастер, ветки), глубоким
# не даются: наружу и в код ходит только он.
expose_mcps=(*CLAUDE_MCPS, *t3code_exposed, *vibegram_exposed),
@@ -409,6 +427,11 @@ TELEGRAM = (
user_id=int(os.environ["TELEGRAM_USER_ID"]),
master_agent="beaver-dispatcher",
branch_agent="beaver-dispatcher",
# §3.8: вложения - входящая папка в зоне агента, по дням; что стоит
# хранить, он переносит к заметке сам, остальное сметается через месяц.
attachments=Attachments(
mode="vault", dir=BEAVER / "вложения", keep_days=30, tz=TZ
),
)
if os.environ.get("TELEGRAM_BOT_TOKEN")
else None
@@ -850,9 +873,9 @@ jobs = [
Job("вайбграм", vibegram, cron="*/10 * * * *", critical=False),
Job("закрытие", close_idle, cron="20 4 * * *", critical=False),
Job("память", memory, cron="30 4 * * 0", critical=False),
# крон в UTC контейнера: 02/10/14/18/22 по Варшаве летом;
# кроны считаются в Gateway.tz (Варшава), не в UTC контейнера;
# руками - POST /api/jobs/куратор/run
Job("куратор", curate, cron="0 0,8,12,16,20 * * *", critical=False),
Job("куратор", curate, cron="0 2,8,12,16,20 * * *", critical=False),
Job("deploy", deploy, webhook=True),
# алерты идут пачками (тревога и отбой подряд) - схлопывать нельзя
Job("komodo", komodo_alert, webhook=True, dedupe=False),
+27 -2
View File
@@ -27,6 +27,7 @@ __all__ = [
"TRIAGE_DISALLOWED",
"Zones",
"bash_zones",
"forbid_skills",
"requires_skill",
"skill_tracker",
"vault_zones",
@@ -220,15 +221,39 @@ def _pathlike(word: str) -> bool:
)
def _skill_name(call: ToolCall) -> str:
"""Имя скилла из `Skill({"skill": "vault:firefly", ...})`, без плагина."""
name = str(call.input.get("skill") or call.input.get("name") or "")
return name.rsplit(":", 1)[-1]
def skill_tracker() -> PolicyRule:
"""Запоминает открытые скиллы в состоянии сессии (для `requires_skill`)."""
def rule(call: ToolCall) -> None:
if call.tool != "Skill":
return
name = str(call.input.get("skill") or call.input.get("name") or "")
opened: set[str] = call.state.setdefault("skills", set())
opened.add(name.rsplit(":", 1)[-1])
opened.add(_skill_name(call))
return rule
def forbid_skills(names: tuple[str, ...], kinds: tuple[str, ...]) -> PolicyRule:
"""§4.3: скиллы из ``names`` (fnmatch, без учёта регистра) в ``kinds`` закрыты.
Мастер их не открывает - для этого есть ветка; правило отвечает так,
чтобы модель сама её открыла.
"""
patterns = tuple(p.lower() for p in names)
def rule(call: ToolCall) -> Deny | None:
if call.tool != "Skill" or call.kind not in kinds:
return None
name = _skill_name(call)
if not any(fnmatch.fnmatchcase(name.lower(), p) for p in patterns):
return None
return Deny(reason=f"скилл {name} недоступен в {call.kind}: открой ветку")
return rule
+24 -3
View File
@@ -4,7 +4,14 @@ from pathlib import Path
import pytest
from beaver_gateway.core.policy import Deny, ToolCall
from policy import Zones, bash_zones, requires_skill, skill_tracker, vault_zones
from policy import (
Zones,
bash_zones,
forbid_skills,
requires_skill,
skill_tracker,
vault_zones,
)
@pytest.fixture
@@ -24,12 +31,14 @@ def zones(vault: Path) -> Zones:
)
def call(vault: Path, tool: str, state=None, **tool_input) -> ToolCall:
def call(
vault: Path, tool: str, state=None, kind: str = "master", **tool_input
) -> ToolCall:
return ToolCall(
tool=tool,
input=tool_input,
agent="a",
kind="master",
kind=kind,
conversation="c",
cwd=vault,
state=state if state is not None else {},
@@ -117,6 +126,18 @@ def test_firefly_requires_open_skill(vault):
assert gate(store) is None
def test_forbid_skills_only_named_kinds_and_only_skill_tool(vault):
rule = forbid_skills(("firefly", "beaver *"), kinds=("master", "job"))
deny = rule(call(vault, "Skill", skill="vault:firefly"))
assert isinstance(deny, Deny)
assert deny.reason == "скилл firefly недоступен в master: открой ветку"
assert rule(call(vault, "Skill", kind="job", skill="Beaver Durden")) is not None
assert rule(call(vault, "Skill", name="vault:FireFly")) is not None
assert rule(call(vault, "Skill", kind="branch", skill="vault:firefly")) is None
assert rule(call(vault, "Skill", skill="общие:заметки")) is None
assert rule(call(vault, "Read", file_path="скиллы/vault/firefly/SKILL.md")) is None
@pytest.fixture
def home(vault: Path) -> Zones:
"""Зоны диспетчера с 2026-08-30: vault - дом, удаление - только своё."""
Generated
+8 -8
View File
@@ -390,7 +390,7 @@ requires-dist = [
{ name = "anthropic", specifier = ">=0.103.0" },
{ name = "anyio", specifier = ">=4.13.0" },
{ name = "argon2-cffi", specifier = ">=25.1.0" },
{ name = "claude-agent-sdk", specifier = ">=0.2.146" },
{ name = "claude-agent-sdk", specifier = ">=0.2.150" },
{ name = "croniter", specifier = ">=6.2.4" },
{ name = "fastapi", specifier = ">=0.136.1" },
{ name = "fastmcp", specifier = ">=3.3.1" },
@@ -502,7 +502,7 @@ wheels = [
[[package]]
name = "claude-agent-sdk"
version = "0.2.146"
version = "0.2.150"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -510,13 +510,13 @@ dependencies = [
{ name = "mcp" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/08/a79f73aad2156f4518ae6e0fffbcdb403cd5347d62b994b005a0c9bb12fb/claude_agent_sdk-0.2.146.tar.gz", hash = "sha256:9e402548466018421cfd0725a7601e14d9e3e879276a6a4a877c5a2ff6633546", size = 344704, upload-time = "2026-08-27T22:25:46.687Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/1e/99bababecf24a4a55a0775665ff0be010aff26f653fc9be2f4f17a0b5ef0/claude_agent_sdk-0.2.150.tar.gz", hash = "sha256:3856e932f8d45514ddf5ddd540fb844678bb11638cabf9b939ce8b708e4eb352", size = 344692, upload-time = "2026-09-01T18:07:15.284Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/e2/7618d7ffd3d4580d53d1147e7baba005c3bf4359d8bb8ab9beab62b5a0ec/claude_agent_sdk-0.2.146-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7bcb91dd34c2e0e5b4f006648d457bbfba9aa1e44cf2e5d02abb441dcbbaff98", size = 84087716, upload-time = "2026-08-27T22:25:50.846Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e6/004b957547e32f6df4948c3a5a3c06974c6c19539c357ebe40a1a23941d3/claude_agent_sdk-0.2.146-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:9285b3dd900376b045a06d2d6f5e19bfd96a3f6cb88d21166bb840aff14ee505", size = 88567306, upload-time = "2026-08-27T22:25:54.876Z" },
{ url = "https://files.pythonhosted.org/packages/37/fa/1c1cb847ff8a92f6ec9a0176a984ec1902cf6eab6c027fd45059d211a96d/claude_agent_sdk-0.2.146-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:ad536806e6c6fc1972ae3950b1bb171f1494e6f47002c24dbc68f19af1c34997", size = 94195787, upload-time = "2026-08-27T22:25:59.367Z" },
{ url = "https://files.pythonhosted.org/packages/60/f9/48aa74e5a85ebe11b916a99b053cc64f47bea8409ec2b9ada5b5a1169a7a/claude_agent_sdk-0.2.146-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:1f2bae0759f3962be2714c786c1a6961824c5ce3d4b7f44f0828191bd44ccebe", size = 94385743, upload-time = "2026-08-27T22:26:03.218Z" },
{ url = "https://files.pythonhosted.org/packages/0d/d4/ff986dc35330146cba60a1bb6ee3f521cdec25ccc0306ef359b9d0cb59c4/claude_agent_sdk-0.2.146-py3-none-win_amd64.whl", hash = "sha256:dc2c4773dc02492b52c74f957a059ecbc0aec7063bd1a16c039b78613194b3cf", size = 97049829, upload-time = "2026-08-27T22:26:08.701Z" },
{ url = "https://files.pythonhosted.org/packages/c3/13/b5b86d90c95e8a691143eeb45bbf2e1c250324817980464d6ea63ef0f789/claude_agent_sdk-0.2.150-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f58d746f3552faeac52883b5a502a8d9b5aef067ea0b68fae5c9bae0f39d73d", size = 85396845, upload-time = "2026-09-01T18:07:19.074Z" },
{ url = "https://files.pythonhosted.org/packages/d9/8f/c17da4fa9cd8bddb8ac1538456dd0fde3a43abb1dfc28ca448f85e2a10d3/claude_agent_sdk-0.2.150-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:fee3ae2dd457742d82070b5fa122070ee8e2e80b4f9b37a57ff0a8261ade656d", size = 89904483, upload-time = "2026-09-01T18:07:22.671Z" },
{ url = "https://files.pythonhosted.org/packages/8d/95/5b33717b807c6fbe29fca55a246eab9a09a80c9dd288ffeb31f05681b7fc/claude_agent_sdk-0.2.150-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:813535a0267271d91af29c456ec26137b34a8b7bc066647ea59efba6e3c26c93", size = 95270310, upload-time = "2026-09-01T18:07:26.61Z" },
{ url = "https://files.pythonhosted.org/packages/9f/de/5a8b59e6a713ea3f4be88816cfd487b1a5b08c8ad9f042abe56d2decc184/claude_agent_sdk-0.2.150-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:cb65ff33060baa098b420232c630fa287d0f95e9469e7ceb706d0b7d8841888e", size = 95460277, upload-time = "2026-09-01T18:07:31.092Z" },
{ url = "https://files.pythonhosted.org/packages/dd/42/d147578a0df16c1b4b0deaebfb3d25e590ad5c16cb6b59a68f9dbb618be3/claude_agent_sdk-0.2.150-py3-none-win_amd64.whl", hash = "sha256:3b517a21c723854de778b38fa51660c5b68ee5fb6f28b2abf291a51471a7b378", size = 98078644, upload-time = "2026-09-01T18:07:35.734Z" },
]
[[package]]