keywright-router
Acts on mail that reaches the agents' mailbox. It is an ordinary Keywright agent (mail-router):
Mail provider ⇄ email-mcp ⇄ Keywright ⇄ router ──(label / draft only)──> Keywright /llm ⇄ LiteLLM
└──(actions)──> Keywright ⇄ email-mcp / ha-mcp / ...
- No secrets. The mailbox password is only in
email-mcp, the provider keys are only in LiteLLM. The router holds a Keywright token, so revokingmail-routerstops it completely. - Routing is boolean. Rules in
rules.yamltest sender, recipients, subject, body and attachments. The model is asked only when a rule reaches alabel:test (one call per message, answer restricted to the configured labels) or an action drafts text withllm:. - Mail never chooses where things go.
send/forwardaddresses andmcptool names are literals in the rules. Message text reaches a call only through{{placeholders}}in free text. The one exception isreply, which answers the sender, and Keywright's recipient rules gate that like any other send. The router never replies to bounces or robots. - Every action is a Keywright call. A call the agent may not make without asking puts a
prompt on the phone. The message's job waits (checked with
access_status, never re-sent), resumes with the identical call once approved, and gives up afterpending_hours. - Leaves your mail alone unless a rule claims it. Handled mail is marked read and filed in
done_folder, or in the folder a rule'smovenames. Mail no rule claimed stays unread in the inbox. Only mail that arrives after the router's first start is considered.
Rules reference: see rules.example.yaml. Commands:
keywright-router check # validate the rules file
keywright-router dry-run # route today's unread mail and print the calls, making none
keywright-router run # the container's command
Setting it up (once)
- Mailbox. Fill in a protected
email.envfile on the broker host (0600; templatedeploy/stack/email.env.example): address, IMAP/SMTP hosts, password, recipient allowlist. - Keywright resource
email(Resources → add MCP server): URLhttp://email-mcp:9557/mcp, default approval, argument keysaccount_name, recipients, cc, bcc, attachments, include_attachments. Each key present in a call adds a path (.../send_email/recipients=a@b.c) and every one must be allowed, so a recipient that is not allowed asks even when the rest of the call is. Standing rules for every agent:- deny
mcp/email/tools/delete_emails,mcp/email/tools/download_attachment,mcp/email/tools/set_email_tags - deny
mcp/email/tools/send_email/attachments=*andmcp/email/tools/forward_email/include_attachments=Trueunless an agent needs them
- deny
- Agent
mail-router(Agents → add). Put its token inrouter.envasKW_TOKEN=kw_.... No rules by hand: on its first start the router works out what its rules need (the mailbox reads, each fixedmcpcall, eachforward/sendrecipient), qualified exactly the way Keywright qualifies the calls, and asks for all of it in one request. Open it under Requests and choose Always allow. It does not poll while that request waits. Replies, values a rule fills from the message, and model access are left out on purpose and ask per call (a model needs a rule in the UI:llm/<gateway>/model/<model>). Preview the list withkeywright-router access; after editing the rules, restart the router and it asks only for what is new. - Rules. Edit
./rules.yaml(seeded from the example). Setllm.gatewayto the name of the LLM gateway resource. - Start. Deploy your chosen email MCP service separately. Install the router from
source with
uv pip install ./client ./router, then runkeywright-router dry-run --config ./rules.yamlbefore enablingkeywright-router run --config ./rules.yaml. SupplyKW_URLandKW_TOKENthrough the environment. This optional integration is not part of the minimal Compose stack.
email-mcp also enforces its own recipient floor (MCP_EMAIL_SERVER_ALLOWED_RECIPIENTS in
email.env). Nothing leaves for an address outside it, whoever approved it.
Web inbox and PDF delivery
Keywright's Mail screen (/ui/mail) adds a searchable inbox, reading pane, PDF downloads,
email rules, and local processing tags: Unprocessed, Queued, Processing, Waiting for approval,
Completed, and Failed. Tags are stored in Keywright, independently of the mailbox's read flag;
opening a message does not mark it read. All indexed mail retains delivery history after a
message leaves the watched folder. The cache indexes up to 10,000 messages in the selected
mailbox and renders message bodies as escaped text, without loading remote email images.
This worker runs inside Keywright and is disabled until configured in Mail → Connection.
Select the existing email resource, account agents, mailbox INBOX, and an enabled agent
(normally mail-router). Mailbox reads, attachment reads, and mutations go through that agent's
existing broker permissions and audit log. Pending approvals park a delivery; a denial or
failure produces a Failed tag and a manual Retry action. Run a single Keywright server worker,
as in the shipped stack; its in-process lock serializes mailbox sync and delivery operations.
The UI rules are separate from the existing YAML rules. Pause overlapping YAML rules (or stop the legacy router container once its jobs have finished) before enabling the new worker. The first sync only indexes existing mail. Subsequently discovered mail can run automatically; existing messages require Run rules. Editing a rule does not re-run old messages. Rules use sender address patterns and subject substrings, with the first enabled match winning in priority order. Actions currently include PDF delivery to Budget, marking read, and moving to a folder.
For PDF delivery:
- Deploy the budget app's document intake feature and create a document delivery token under Settings → Document inbox. This is separate from its Home Assistant summary token.
- In Mail → Connection, enter the budget app's direct server URL and delivery token. The
endpoint is fixed to
/api/integrations/documents. Do not use a Home Assistant ingress URL. - On email-mcp 1.9.1, set
MCP_EMAIL_SERVER_ENABLE_ATTACHMENT_CONTENT=trueinemail.envand recreate that service. For managed email-server configurations, enable the corresponding attachment-content policy instead. This does not enable filesystem attachment downloads. - Allow
get_attachment_contentfor the chosen agent/account, along with metadata/body reads and marking read. A deny ondownload_attachmentcan remain: the UI uses embedded binary resources. - Add an electric-bill rule with Send PDFs to Budget. Test it on one existing message before enabling automatic processing.
Each PDF is transferred byte-for-byte, without parsing or changing budget amounts. The worker accepts PDFs up to 5 MiB each (within email-mcp's 8 MiB serialized-result ceiling). Budget confirms the SHA-256 checksum, and Keywright stores the receipt before continuing. Partial delivery retries skip confirmed files; Budget also deduplicates a delivery if the response was lost. Mail is marked read only after all PDFs are confirmed. Completed mail stays in the inbox. Destination changes do not silently redirect an existing job: restore its original destination before retrying it.
The budget token stays in Keywright's protected settings database and is never rendered back into the form or included in audit details. Back up that database as a credential-bearing file.
License
Keywright is licensed under Apache-2.0. Copyright 2026 Keywright contributors. See NOTICE and third-party notices. Commercial use and proprietary extensions are permitted under the license's terms. Unless explicitly stated otherwise, contributions intentionally submitted for inclusion in Keywright are provided under Apache-2.0.
Metadata
Release files for keywright-router 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| keywright_router-0.0.1.tar.gz | 25.5 kB | With provenance | Details for keywright_router-0.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| keywright_router-0.0.1-py3-none-any.whl | Python 3 | none | any | Details for keywright_router-0.0.1-py3-none-any.whl |
Total release size: 54.7 kB
Release files / keywright_router-0.0.1.tar.gz
| Download URL | keywright_router-0.0.1.tar.gz |
|---|---|
| Size | 25.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b40d791a22ca5d74eaaa0f78fca1e3733bb6516f815cfb94c276406844bc8028
|
|
BLAKE2b-256 checksum How to use checksums |
e97f428b8004184308379bbf7e9ea7e0e81326e133ba4236c42e9ac6971dcec5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logRelease files / keywright_router-0.0.1-py3-none-any.whl
| Download URL | keywright_router-0.0.1-py3-none-any.whl |
|---|---|
| Size | 29.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
135736060ef938c701bc7fe648d80ebe3d7059b5c67820c2b5e4b599e8279746
|
|
BLAKE2b-256 checksum How to use checksums |
622ec496d5b7973bb63fbf2eb8190ff25386bc2c4bbd455129f23c9cfbb7a8a5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logSecurity for keywright-router 0.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for keywright_router-0.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.