Compare commits

..
4 Commits
8 changed files with 64 additions and 5 deletions

No files matched your search

+5
View File
@@ -31,6 +31,11 @@ ADMIN_PASS=
# Подпись session-cookie admin UI # Подпись session-cookie admin UI
SESSION_SECRET= SESSION_SECRET=
# Мастер-ключ хранилища секретов (admin → System → Secrets): openssl rand -hex 32.
# Шифрует таблицу secrets в postgres, поэтому дамп базы без него закрыт.
# В env модели не попадает. Потерян или сменён - секреты задаются заново.
SECRETS_KEY=
### Claude #################################################################### ### Claude ####################################################################
# OAuth-токен подписки для Claude Agent SDK внутри контейнера. # OAuth-токен подписки для Claude Agent SDK внутри контейнера.
+6
View File
@@ -71,6 +71,12 @@ read-write (it is the dispatcher's home), and `beaver_agent/policy.py` keeps
it out of `.obsidian`, `мета` outside `мета/бобер`, and from deleting it out of `.obsidian`, `мета` outside `мета/бобер`, and from deleting
anything but its own files. anything but its own files.
Keys the agent needs for its own scripts (an RPC key, a service account
file) go into **System → Secrets** in the admin, not into files in the
container: they are sealed with `SECRETS_KEY` in Postgres, never shown back,
and reach a command only through `secret run NAME -- cmd` (see the `secrets`
skill under `мета/бобер/скиллы/общие/`).
### 4. Mint a token ### 4. Mint a token
Open admin at `http://localhost:62990/` (or `https://<DOMAIN>/` if Caddy), sign in with `ADMIN_USER` / `ADMIN_PASS`, go to **Tokens → Create**, scope `*` for first run. Open admin at `http://localhost:62990/` (or `https://<DOMAIN>/` if Caddy), sign in with `ADMIN_USER` / `ADMIN_PASS`, go to **Tokens → Create**, scope `*` for first run.
+3 -1
View File
@@ -1,4 +1,4 @@
"""Ночью после ротации: глубокие чаты, тихие два дня, закрываются, не больше трёх.""" """Ночью после ротации: тихие глубокие чаты - с выжимкой, тихие ветки - молча."""
from __future__ import annotations from __future__ import annotations
@@ -21,3 +21,5 @@ async def close_idle(run: JobRun) -> None:
result.digest.path if result.digest else None, result.digest.path if result.digest else None,
result.error, result.error,
) )
for branch in await run.close_quiet(kind="branch", days=3):
_log.info("closed quiet branch %s «%s»", branch.external_id, branch.title)
+7 -2
View File
@@ -1,4 +1,8 @@
"""Раз в час: пора ли сменить мастера (ночь, возраст, размер контекста).""" """Раз в час: пора ли сменить мастера (ночь, возраст, размер контекста).
Запуск руками (кнопка в админке, `POST /api/jobs/ротация/run`) ротирует мастера
сразу, с хендаутом: так новая сессия подхватывает свежие гранулы vault.
"""
from __future__ import annotations from __future__ import annotations
@@ -12,5 +16,6 @@ _log = logging.getLogger(__name__)
async def rotate(run: JobRun) -> None: async def rotate(run: JobRun) -> None:
for master in await run.rotate(): force = "manual" if run.trigger == "manual" else None
for master in await run.rotate(force=force):
_log.info("rotated master -> %s", master.external_id) _log.info("rotated master -> %s", master.external_id)
+7 -2
View File
@@ -1,4 +1,4 @@
"""`POST /hooks/t3code`: тред завершился, упал или спрашивает - срочно в мастер.""" """`POST /hooks/t3code`: событие треда - срочно в разговор, который его запустил."""
from __future__ import annotations from __future__ import annotations
@@ -15,7 +15,12 @@ QUESTION_HINT = (
async def t3code_event(run: JobRun) -> None: async def t3code_event(run: JobRun) -> None:
await run.inject_master(describe(run.payload), urgency="urgent", origin="t3code") await run.inject_into(
run.payload.get("origin"),
describe(run.payload),
urgency="urgent",
origin="t3code",
)
def describe(p: dict[str, Any]) -> str: def describe(p: dict[str, Any]) -> str:
+1
View File
@@ -151,6 +151,7 @@ SAME_DAY = (
ROTATION_WHY = { ROTATION_WHY = {
"age": "прошло больше 36 часов", "age": "прошло больше 36 часов",
"context": "контекст стал слишком большим", "context": "контекст стал слишком большим",
"manual": "Бобёр запустил ротацию руками",
} }
+33
View File
@@ -0,0 +1,33 @@
from typing import Any
from beaver_agent.jobs.t3code import t3code_event
class FakeRun:
def __init__(self, payload: dict[str, Any]) -> None:
self.payload = payload
self.injects: list[tuple[str | None, str, str, str]] = []
async def inject_into(
self, public_id: str | None, text: str, *, urgency: str, origin: str
) -> bool:
self.injects.append((public_id, text, urgency, origin))
return True
async def test_the_event_goes_to_the_conversation_that_dispatched() -> None:
run = FakeRun(
{
"event": "completed",
"thread_id": "t1",
"title": "lint",
"machine": "mac",
"project": "beaver",
"origin": "conv-7",
"text": "done",
}
)
await t3code_event(run) # ty: ignore[invalid-argument-type]
[(target, text, urgency, origin)] = run.injects
assert (target, urgency, origin) == ("conv-7", "urgent", "t3code")
assert "завершён" in text and "done" in text
Generated
+2
View File
@@ -365,6 +365,7 @@ dependencies = [
{ name = "argon2-cffi" }, { name = "argon2-cffi" },
{ name = "claude-agent-sdk" }, { name = "claude-agent-sdk" },
{ name = "croniter" }, { name = "croniter" },
{ name = "cryptography" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "fastmcp" }, { name = "fastmcp" },
{ name = "greenlet" }, { name = "greenlet" },
@@ -392,6 +393,7 @@ requires-dist = [
{ name = "argon2-cffi", specifier = ">=25.1.0" }, { name = "argon2-cffi", specifier = ">=25.1.0" },
{ name = "claude-agent-sdk", specifier = ">=0.2.157" }, { name = "claude-agent-sdk", specifier = ">=0.2.157" },
{ name = "croniter", specifier = ">=6.2.4" }, { name = "croniter", specifier = ">=6.2.4" },
{ name = "cryptography", specifier = ">=48.0.0" },
{ name = "fastapi", specifier = ">=0.136.1" }, { name = "fastapi", specifier = ">=0.136.1" },
{ name = "fastmcp", specifier = ">=3.3.1" }, { name = "fastmcp", specifier = ">=3.3.1" },
{ name = "greenlet", specifier = ">=3.5.0" }, { name = "greenlet", specifier = ">=3.5.0" },