diff --git a/surfsense_backend/app/capabilities/__init__.py b/surfsense_backend/app/capabilities/__init__.py new file mode 100644 index 000000000..d159536fb --- /dev/null +++ b/surfsense_backend/app/capabilities/__init__.py @@ -0,0 +1,5 @@ +"""Scraper capability registry — typed, stateless verbs. See plans/backend/04-capabilities.md.""" + +from __future__ import annotations + +__all__: list[str] = [] diff --git a/surfsense_backend/app/capabilities/types.py b/surfsense_backend/app/capabilities/types.py new file mode 100644 index 000000000..0a9c261e8 --- /dev/null +++ b/surfsense_backend/app/capabilities/types.py @@ -0,0 +1,47 @@ +"""``Capability`` registry contracts shared by every verb.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from enum import StrEnum +from typing import TYPE_CHECKING, Any, Protocol + +if TYPE_CHECKING: + from pydantic import BaseModel + from sqlalchemy.ext.asyncio import AsyncSession + + +class BillingUnit(StrEnum): + """The meter a verb charges on (priced by the billing service, 03c).""" + + WEB_CRAWL = "web_crawl" + + +class BillableOutput(Protocol): + """A capability output that reports its own billable count.""" + + @property + def billable_units(self) -> int: ... + + +@dataclass(frozen=True) +class CapabilityContext: + """Request-scoped deps a capability call needs beyond its typed input.""" + + session: AsyncSession + workspace_id: int + + +Executor = Callable[[Any], Awaitable[Any]] + + +@dataclass(frozen=True) +class Capability: + """One typed verb; the source of truth the doors (05) and agent (07) read.""" + + name: str + input_schema: type[BaseModel] + output_schema: type[BaseModel] + executor: Executor + billing_unit: BillingUnit