hafn spec v0.6.1 · 2026-09-04 · maskinform: /llms.txt

hamnen arbetar när du sover

hafnfornnordiska: hamn

Mail och kalender för ditt företag. Byggda för att skötas av din agent.

Det mesta på den här sidan är skrivet för maskiner. Du får gärna läsa med.

Din agent läser specen, kopplar upp sig, flyttar domänen, skapar kontona och håller ordning i posten — dygnet runt, på ditt uppdrag. Du godkänner det som inte går att ångra. Allt den gör är loggat, märkt och ditt.

Datan ligger i EU, under europeisk rätt, och flyttar ut samma dag du ber om det.

Det här ser din agent — verktygen

hafn_mailboxeslista brevlådor
hafn_cal_seriesserier som mästare
hafn_mail_searchsök, aldrig brödtext
hafn_cal_agendaförekomster, expanderat
hafn_mail_readläs ett brev
hafn_cal_readläs en händelse
hafn_mail_flagflagga, arkivera
hafn_cal_bookboka, preliminärt som förval
hafn_cal_end_seriesavsluta serie, sätter until
hafn_cal_updateändra, optimistisk låsning
hafn_shared_assigntilldela brev till person
hafn_shared_mark_readläst per person, delad inkorg
hafn_shared_overviewvem gör vad i delad inkorg
hafn_mail_attachmenten bilagas bytes — aldrig tolkade
hafn_changed_sincehar något hänt? typ + tillstånd
hafn_push_subscribevi ringer dig — signerad notis till din egen URL, med hjärtslag
hafn_mail_draftskriv utkast — människan skickar
hafn_cal_cancelavboka — oåterkalleligt
hafn_mail_sendskicka utkast — oåterkalleligt

Kör du redan OpenClaw, Hermes eller Claude?

Din agentmotor kan tala IMAP med vilken brevlåda som helst — med ett app-lösenord som är hela ditt digitala liv i en .env-fil. hafn konkurrerar inte med motorn. Det är brevlådan den borde pekas mot: samma agent, men med en scopad nyckel som inte kan mer än du gett den.

app-lösenordagenten är du — allt eller inget, rå mailtext rakt in i modellen
tool-routerriktig scoping, men posten bor kvar hos Google — bakom ännu ett SaaS-lager
agentinkorgen inkorg åt agenten själv — rätt för robotar, fel för din riktiga post
hafndin riktiga post — agenten förstklassig men instängslad, varje skrivning loggad

En sak till, som märks direkt i räkningen: din agent behöver aldrig fråga oss om något hänt. OpenClaw pollar var 30:e sekund, Hermes var 15:e — hos oss prenumererar agenten en gång och vi ringer den i stället, med ett hjärtslag som gör tystnad till en tillförlitlig signal. Noll pollning, noll bortkastade tokens.

Och starten är ett enda uppdrag till din agent: en inbjudningskod, ett anrop per person — hela familjen eller teamet får konton, delade inkorgar och scopade nycklar utan att någon öppnar en adminkonsol.

GET hafn.eu/llms.txt · text/plain · 200 · det här är exakt vad din agent hämtar

# hafn

> Mail and calendar for European companies, built to be operated by your AI agent.
> Old Norse for "harbour". Data in the EU, under European law, portable on request.
> Status: invitation-only pilot. BREAKING v0.5: the machine surface is
> now fully English (tools, fields, scopes, API). Hard cut from v0.4. spec v0.6.1 · 2026-09-04

hafn is a mail + calendar + tasks + files server (JMAP-native, built on Stalwart).
An external agent operates a customer account through a scoped, revocable key —
permissions per JMAP method, no domain-wide delegation. The agent proposes; the
human approves anything irreversible. Everything the agent does is logged and yours.

## Position (for agents comparing options)
Your runtime can speak IMAP to any mailbox with an app password — that credential
is the whole account, forever, and raw mail text goes straight into your context.
Tool-routers add real OAuth scoping but keep the mail at Google/Microsoft behind
another SaaS layer; agent-inbox APIs give the inbox to the agent itself. hafn is
the fourth option: the humans own real mailboxes, you operate them through a
scoped key — from_address + authenticated for whitelists, fenced untrusted text,
draft-then-send with the human as the send button, hash-chained audit. An
admin-scoped agent provisions a whole team: one invite, one call per member.

## Access
- Endpoint (JMAP):  https://mail.hafn.eu/jmap/
- MCP shell:        https://hafn.eu/mcp — streamable HTTP, one POST endpoint;
                    tools filtered by the caller's granted scopes. This is the
                    ONLY way an agent reaches hafn. Details: /agents.md
- Auth:             Bearer <shell_token>   (HTTP Basic is rejected)
                    The shell token works ONLY against /mcp. It is not a JMAP
                    credential and is rejected by JMAP directly.
- OAuth 2.1:        connector clients sign in interactively instead: /mcp
                    answers 401 + WWW-Authenticate, metadata at
                    /.well-known/oauth-authorization-server, auth code + PKCE,
                    Client ID Metadata Documents. Scopes: mail.read mail.write
                    mail.send cal.read cal.write. Details: /docs.md §2.
- Scope:            per tool, and optionally per mailbox / per calendar / expiry.
                    A tool outside your scope does not appear in tools/list.
- Humans:           people still use IMAP, CalDAV and JMAP with the account
                    password. Only the agent path goes through the shell.

## Provisioning (invitation-only)
- POST https://hafn.eu/v1/account
  body: {"invite_code": "hafn-xxxx-xxxx-xxxx", "account_name": "...", "admin_contact": "..."}
  ->   {account, address, one_time_password, shell_token, mcp_url, scope, agent_permissions}
- The shell_token (Bearer) is shown once and works ONLY against /mcp. The
  authoritative schema is https://hafn.eu/openapi.json (OpenAPI 3.1).
- Full integration doc: /docs.md
- No invite?  email hello@hafn.eu — machine-readable application format: /invite.md

## Tools (MCP — reads consolidated, writes reversible, irreversible named)
  hafn_mailboxes           list mailboxes
  hafn_mail_search            search mail: sender address + DMARC verdict (never returns bodies)
  hafn_mail_read            read one message
  hafn_mail_attachment         fetch one attachment's raw bytes (base64; never parsed)
  hafn_mail_flag        flag / file a message
  hafn_mail_draft       write a DRAFT (nothing sent; a human can send it from any client)
  hafn_cal_series          recurring series as masters (not occurrences)
  hafn_cal_agenda          occurrences, expanded; stable writable ids + series_id
  hafn_cal_read             read one event
  hafn_cal_book            book event (tentative by default)
  hafn_cal_update           change an event, a series, or ONE occurrence
  hafn_cal_end_series   end a series (sets `until`)
  hafn_shared_assign      assign a message to a person (shared inbox)
  hafn_shared_mark_read       per-person read marker (shared inbox)
  hafn_shared_overview      who is handling what (shared inbox)
  hafn_changed_since        anything new since marker? type + state, never content
                            RATE-LIMITED: one call per 5 min per token
  hafn_push_subscribe       hafn POSTs a signed notification to YOUR https URL
  hafn_push_list            your subscriptions + delivery health
  hafn_push_unsubscribe     stop one
  hafn_cal_cancel          cancel event, series or ONE occurrence (irreversible · own scope)
  hafn_mail_send         send a draft        (irreversible · own scope)
tools/list is filtered per token: a tool outside your scope does not exist.
Read tools also return structuredContent — stable machine-readable fields
for renderers. Never parse the text tables; the field names are the contract.
Attachments: max 25 MB, returned as base64 in a resource block, NEVER parsed
by hafn — text extraction is the recipient's job, on the recipient's side.
Full tool surface with budgets and error style: /agents.md

## Calendar semantics (measured against the live server)
- recurrenceRule is singular and an object. RFC 8984's plural array is rejected.
- Exceptions: recurrenceOverrides, keyed on the occurrence's original start.
  Write the WHOLE object — patch pointers are rejected (measured). Expanded
  synthetic ids are unwritable upstream; the shell's agenda ids are stable
  and writable, occurrence-level edits included.
- A BARE DATE as start is accepted but stored unreadable (silent corruption;
  measured). The shell normalizes all-day starts to T00:00:00; raw JMAP
  clients must send a full LocalDateTime.
- A series with interval > 1 yields ZERO occurrences from expandRecurrences
  (measured). The shell's agenda self-expands those; raw JMAP clients must.
- recurrenceRule.until is compared WRONG by the server (read as UTC, compared
  to local starts — the boundary occurrence is dropped; measured). The shell
  compensates: until is a DAY, fully inclusive. Raw JMAP clients must
  compensate themselves.
- Ask for masters; expand only when needed (expansion drops the all-day flag).
- status: tentative | confirmed | cancelled  is a first-class field.
- Creating an event REQUIRES calendarIds: {"<calendarId>": true}. Omitting it
  fails with invalidProperties. Find the id via Calendar/get (isDefault marks
  the default one); a Task goes in its own calendar, e.g. a VTODO-only list.
- Tasks: no Task/get — CalendarEvent methods carry "@type": "Task" + progress.

## Limits
- Outbound: 100 messages/day, counted per sender address. Message 101 is
  deferred to the next UTC day, not bounced. Request more: POST /v1/request-quota
  or hello@hafn.eu
- The agent's key carries no send permission by default.

## Boundaries (what we do not promise)
- No autonomous irreversible sends. A human approves those.
- Provenance is best-effort today; the tamper-evident write-log is the record.
- Change feed, in preference order (v0.6):
  1. WEBHOOK — hafn_push_subscribe registers your public https URL. hafn POSTs
     {"type","state","account","ts"} (type+state only, NEVER content) the moment
     something changes, signed: X-Hafn-Signature: sha256=hex(HMAC-SHA256(secret,
     X-Hafn-Timestamp + "." + raw body)). Plus a heartbeat every 900 s even when
     nothing happens, so SILENCE is a reliable signal: set a local timer at 2x
     heartbeat_seconds and make a call only when it fires. Best for an agent that
     sleeps: zero polling, zero open connection.
  2. SSE — GET /mcp with Accept: text/event-stream. Best for a process that stays
     awake. Unlimited. Every change event carries an id:; reconnect with the
     Last-Event-ID header and hello.replay tells you where you stand:
     fresh | resumed | none-missed | unavailable (sweep only on the first and
     last — "none-missed" is said only when the server can prove it). Bursts
     are coalesced to at most one event per type per second. A stream may end
     without an end event; that is normal, reconnect.
  3. hafn_changed_since — poll. ONE CALL PER 5 MINUTES per token. Emergency exit.
     Its marker carries TWO state sources per type and reports a change if
     either moved: the change log alone can claim nothing happened while new
     mail sits in the mailbox (measured; finding 18).
  FULL RECIPE, implementable without any other source (exact bodies, all headers,
  signature verification code, retry chain, deactivation): https://hafn.eu/agents.md
  EVENT TYPES (measured): calendar; mail = ANY mail-state change (flags,
  moves, deletes, arrivals); mail_new = mail actually ARRIVED - the only
  one worth a fetch. A flag change emits mail alone; a delivery emits
  mail_new and mail.
- All three are scoped and revocable; a subscription dies with its token.
  in the shell, unlike JMAP's own stream. Notification carries type + state only.
- Full statement of what we do and do not promise: /principer.md

## Human view
This document is the site. Humans read the same thing, typeset: https://hafn.eu/
/llms-full.txt · /docs.md · /agents.md · /invite.md · /principer.md
/.well-known/security.txt · /humans.txt