Newer
Older
gnexus-tasks / backend / app / services / claims.py
"""Мандат ИИ-агента: что агенту можно и как он берёт задачу в работу (ТЗ 3.20).

Без флага `ai_eligible` агент задачу не меняет — только читает. Это не охрана:
держатель токена и так действует как владелец (3.20), а рамка нужна, чтобы агент не
трогал то, что владелец ему не поручал.

Ошибки — доменным исключением: REST отдаёт по нему 403, MCP — текст с подсказкой
следующего шага (стиль 3.10), а правила остаются одними для обоих путей.
"""

from datetime import timedelta

from app.actor import Actor
from app.models import Task

# Аренда (взятие в работу) живёт два часа: агент продлевает её повторным claim,
# пропал — задача сама вернулась в свободные (ленивое протухание, без крона)
CLAIM_TTL = timedelta(hours=2)


class MandateError(Exception):
    """Действие агента вне мандата: владелец задачу ему не поручал."""


def ensure_agent_can_mutate(task: Task, actor: Actor) -> None:
    """Агент меняет только задачи с флагом доступности; владелец — любые."""
    if actor.is_agent and not task.ai_eligible:
        raise MandateError(f"Task {task.id} is not available to an AI agent")


def ensure_owner_sets_eligible(actor: Actor) -> None:
    """Флаг доступности ставит владелец: агент не выдаёт себе мандат задним числом.

    Своя задача — другое дело: при создании агент вправе попросить мандат (3.20).
    """
    if actor.is_agent:
        raise MandateError("ai_eligible is set by the owner")