OAKENMAIL
Generated

Plugin contracts

The signatures, data types and capabilities of the five plugin kinds, generated from the code.

This page is generated from src/oaken/plugins/contracts.py and src/oaken/plugins/base.py, so it always matches the code. For a walkthrough, see Writing a plugin.

The plugin API version is 1. A plugin whose api_version differs is rejected at load.

1

Kinds, entry points and capabilities

Kind Entry-point group Capabilities (and the method each needs) Required members
provider oaken.providers auth (authenticate()), sync.pull (pull()), flags.write (set_flags()), send (send()) none
embedder oaken.embedders none model_id, dimensions, embed
llm_backend oaken.llm_backends none model_id, context_window, generate
enricher oaken.enrichers none version, needs_llm, enrich
vector_store oaken.vector_stores none upsert, delete, query, stats
2

Data types

Frozen dataclasses of plain data. These cross the boundary.

PluginInfo

What the host knows about a loaded plugin.

Field Type Default
name str required
version str required
kind PluginKind required
capabilities frozenset[str] required
api_version int required

PluginContext

Handed to start(). config is the plugin's own [plugins.<name>] table.

Field Type Default
name str required
kind PluginKind required
settings Settings required
clock Clock required
log Any required
config dict[str, Any] empty
bus EventBus | None None
account_id str | None None
llm LLMAccess | None None
_subscriptions list[Callable[[], None]] empty

Health

Field Type Default
ok bool required
detail str ''

PluginOpContext

What an operation handler of a plugin receives besides its input. No database handle: a plugin reaches its own service, its config and the clock, nothing else.

Field Type Default
plugin str required
client str required
actor str required
config dict[str, Any] required
clock Clock required
log Any required

PluginChange

One line of a proposal: what would change. target is an id, never mail text.

Field Type Default
target str required
field str ''
before Any None
after Any None

PluginPreview

What a mutating operation would do, shown to the person before it does it.

Field Type Default
summary str required
changes tuple[PluginChange, ...] ()
total int 0

PluginOperation

One operation a plugin offers. permission is read or mutate (a plugin cannot send). A mutate is confirmed by the person at the CLI or UI, and by proposal and token over MCP; give it a preview to say what it would change. mcp is off (the user may turn it on) or never (they may not): a plugin's operations are never on by default. input and output are pydantic models.

Field Type Default
name str required
summary str required
permission Literal['read', 'mutate'] required
input type[BaseModel] required
output type[BaseModel] required
handler PluginOpHandler required
preview PluginOpPreview | None None
mcp Literal['off', 'never'] 'off'

RawMessage

One message exactly as the server holds it.

Field Type Default
provider_id str required
raw_bytes bytes required
provider_labels tuple[str, ...] ()
provider_flags frozenset[str] frozenset()
internal_date datetime | None None

FlagChange

A flag change made on the server side (for example, read on another device).

Field Type Default
provider_id str required
add frozenset[str] frozenset()
remove frozenset[str] frozenset()

PullBatch

One page of changes. next_cursor is opaque: core stores it and hands it back.

Field Type Default
messages tuple[RawMessage, ...] ()
deleted_provider_ids tuple[str, ...] ()
flag_changes tuple[FlagChange, ...] ()
next_cursor str | None None

LLMRequest

system is a static instruction. Mail content only ever appears in user, as UntrustedText (plain str parts are for the application's own words).

Field Type Default
system str required
user tuple[str | UntrustedText, ...] ()
json_schema dict[str, Any] | None None
max_output_tokens int | None None
temperature float 0.0

LLMResponse

Field Type Default
text str ''
data dict[str, Any] | None None
input_tokens int 0
output_tokens int 0
model_id str ''

EnrichAttachment

What the message says about one attachment. The bytes are never passed to an enricher.

Field Type Default
filename UntrustedText | None required
content_type str required
size int required
inline bool False
quarantine bool False

EnrichInput

A cleaned message. Everything derived from the mail is UntrustedText. headers holds a few display values (from). raw_headers has every header, keys in lower case and every value kept, for a deterministic enricher such as the inspector. parent_summary is the stored summary of the message this one replies to, when core has one: a reply is summarized from its own new text plus that, never from the parent's body.

Field Type Default
message_id str required
account_id str required
subject UntrustedText required
body UntrustedText required
headers dict[str, UntrustedText] empty
raw_headers dict[str, tuple[UntrustedText, ...]] empty
attachments tuple[EnrichAttachment, ...] ()
date datetime | None None
is_reply bool False
parent_summary UntrustedText | None None

EnrichOutput

A JSON-serializable result, which must match the enricher's output_schema.

Field Type Default
data dict[str, Any] required

VectorItem

One vector. The model and dimensions are recorded per vector; a store never mixes them.

Field Type Default
message_id str required
chunk_id str required
vector tuple[float, ...] required
model_id str required
content_hash str required
account_id str | None None

Hit

Field Type Default
message_id str required
chunk_id str required
score float required
model_id str ''

VectorStats

Field Type Default
count int required
model_id str | None None
dimensions int | None None
3

Protocols

A plugin satisfies a contract by having these members.

LLMAccess

The one way a plugin reaches a language model: a PromptBuilder task about mail, as UntrustedText parts. Core picks the workspace from the account, enforces what it allows, answers a repeated question from its cache and validates the answer against the task's schema. A plugin never sees a backend, a database or a workspace.

async def generate_cached(self, builder: 'PromptBuilder[Any]', *parts: UntrustedText, content_hash: str, account_id: str | None=None, message_id: str | None=None) -> Any

Plugin

Lifecycle, shared by every kind. All three are optional for a plugin to define.

async def start(self, ctx: PluginContext) -> None

Acquire resources. Raising marks the plugin unhealthy; core carries on.

async def stop(self) -> None

Release resources. Called once at shutdown.

async def health(self) -> Health

A cheap liveness check, polled periodically by the host.

Provider

A mail source or a way to send. Capabilities: auth, sync.pull, flags.write, send. A provider declares at least sync.pull or send: a send-only provider (an SMTP server) has no mail to pull.

async def authenticate(self, ctx: PluginContext) -> None

Make sure the plugin can talk to the service (capability auth).

def pull(self, cursor: str | None, folder: str | None=None) -> AsyncIterator[PullBatch]

Yield batches of changes since cursor (None: from the start). Capability sync.pull. Re-pulling from the last cursor returns nothing new. folder limits the pull to one folder, for a provider with folders (set supports_folders = True); the cursor is still the account's whole cursor, and a folder pull only moves that folder's part of it. Core passes folder only to a provider that supports it, and only when the user scheduled that folder on its own.

async def set_flags(self, provider_ids: Sequence[str], add: Collection[str], remove: Collection[str]) -> None

Write flags back to the server (capability flags.write).

async def send(self, raw_bytes: bytes) -> str

Send a message and return its provider id. Declared for the future; unimplemented in v1, and the host never routes sending to a plugin (capability send).

Embedder

Turns text into vectors.

Attributes:

async def embed(self, texts: Sequence[str]) -> list[list[float]]

One vector of dimensions floats per text, in order.

LLMBackend

Runs a language model.

Attributes:

async def generate(self, request: LLMRequest) -> LLMResponse

Answer request. With a json_schema, data carries the parsed result.

Enricher

Derives something from one message. version is part of the cache key: bump it when the output could change.

Attributes:

async def enrich(self, msg: EnrichInput) -> EnrichOutput

Derive this enricher's result for msg.

VectorStore

Stores vectors and finds the nearest ones.

async def upsert(self, items: Sequence[VectorItem]) -> None

Insert or replace by (message_id, chunk_id). Raises if an item's model or dimensions differ from what the store already holds, leaving the store unchanged.

async def delete(self, message_ids: Sequence[str]) -> None

Remove every vector of these messages.

async def query(self, vector: Sequence[float], k: int, filters: dict[str, str] | None=None) -> list[Hit]

The k nearest, best first. filters may name account_id and model_id; a store that sits beside the index may take more (thread_key, after, before, sender) and must refuse a filter it does not know rather than ignore it. Raises if vector does not match the stored dimensions.

async def stats(self) -> VectorStats

How many vectors, and which model and dimensions they use.

4

Base classes

Optional. They supply defaults so a plugin overrides only what it does.

BaseProvider

Override pull (a mail source) or send (a sender) at least. Add authenticate and set_flags as the service allows. Capabilities come from what you override (pull is sync.pull, set_flags is flags.write). To state them yourself, set capabilities = frozenset({...}); the host rejects a capability whose method you did not write.

Set:

Override:

BaseEmbedder

Set:

Override:

BaseLLMBackend

Set:

Override:

BaseEnricher

Set version and, ideally, output_model (a pydantic model): the host then checks the result against it and publishes its JSON schema. Override enrich.

Set:

Override:

BaseVectorStore

Set:

Override:

The @enricher decorator

def enricher(name: str, *, version: str='1', needs_llm: bool=False, output: type[BaseModel] | None=None) -> Callable[[Callable[[EnrichInput], Awaitable[object]]], type[BaseEnricher]]

Turn one async function into an enricher class, ready to register as an entry point: class Result(BaseModel): words: int @enricher("word-count", output=Result) async def word_count(msg: EnrichInput) -> Result: return Result(words=len(msg.body.unwrap().split())) The function may return the model, a dict or an EnrichOutput; the host coerces it.