OAKENMAIL
Pre-alpha

Operations

The CLI, the web UI and the MCP server do not each decide what is safe. They all call one registry of typed operations, and the rules live in code: a permission class on every operation, a confirm gate on every change, and a ceiling on what an agent may ever be offered.

1

One registry, three doors

An operation has a name, a one-line summary, input and output models, a permission class and a handler. The full list is generated from the registry. The OpenAPI 3.1 document is derived from the same models, so it cannot disagree with the code.

A caller says which door it came through: cli, ui, mcp or internal. An mcp caller must be an agent and a cli or ui caller must be a person. Only a person can confirm.

2

Permission classes

Class Meaning
read Changes nothing, and never marks mail read.
mutate Changes something. Needs confirming.
send Reserved. Refused in this version, and never offered over MCP.

Defining an operation that breaks a rule fails at import, so it cannot ship: a send that could be offered over MCP, a read that marks mail read, or a mutate an agent can reach that cannot say what it would change.

3

The confirm gate

Every call passes the same checks, in this order:

  1. Exposure. An MCP caller gets "not available" for anything not offered to it, in the same words whether it is switched off or can never be offered.
  2. Input. It must parse. The error names the field, never the value.
  3. Class. send is refused.
  4. Confirmation, for mutate.
    • A person at the CLI or UI confirms by passing confirm, meaning they asked for exactly this.
    • An agent cannot. It gets a proposal: what would change (ids with before and after, never mail text), a count, and a single-use token. Nothing has changed yet. It calls again with the token, which is accepted only for the same operation, the same input and the same caller.
  5. Run, validate the output, audit.

Tokens are random, stored only as a hash, last two minutes by default, are burned by any attempt to use them, and a caller can hold only so many at once.

4

What an agent is offered

Each operation has a ceiling fixed in code: never, off or on.

You can lower an operation, or a whole plugin, from the default, and raise one up to its ceiling. The setting is one global list in the database, changed through set_operation_exposure, which is never available to an agent, so an agent cannot widen its own reach. Each change is audited. Per-token scoping is left for the MCP server.

5

Read state

Reading never marks mail read. The handler that marks it is the one place that does, after the gate has confirmed it, and a test keeps the registry, the gate and the plugin bridge from naming the read-state service at all.

summarize_message is a mutate with an option to mark the messages read afterwards. A summary on its own never marks anything read: opening a message is the only automatic reason left. Several messages can be summarized in one call (20 by default), then marked read together in one confirmed step.

6

Limits

Results are bounded, and the bounds are settings in [operations]. When in doubt, it is a setting.

Setting Default Meaning
max_results / mcp_max_results 50 / 20 Rows in one listing, for you and for an agent
max_chars / mcp_max_chars 20000 / 8000 Mail text in one answer
snippet_chars 240 One snippet
max_batch_ids 200 Ids in one mark or undo
max_summarize_ids 20 Messages in one summary call
max_raw_bytes 256 KiB Raw bytes in one answer
confirm_ttl_seconds 120 How long a proposal lasts
max_pending_confirmations 1000 Open proposals
proposal_changes_shown 50 Changes listed in a proposal

A request over a count limit is refused, not cut, so nothing is silently skipped. Text is cut and the answer says it was.

7

Plugin operations

A plugin can offer operations, named <plugin>.<name>. They get the same gate, the same audit and the same ceiling rules, with these differences:

8

What is not built