Algen AgentTOOLS Algen AgentTOOLS Documentation
Algen Agent Tools · v0.2.0a10

Typed tools for agents. Governed by default.

Curated connectors for agents built on Algen Agent Runtime. Each connector is a set of narrow, typed actions with a declared side effect, permission and approval rule, registered as a Runtime plugin so Studio can list it before anything runs.

0.2.0a10 · pre-alpha Python 3.12 & 3.13 Runtime >=0.1.0a23,<0.2 Apache-2.0

Where Tools fits#

Runtime runs agents; Tools gives them safe ways to touch the outside world. A connector's tools reach Runtime through a declared plugin, Studio lists them from algen-agent-runtime describe, and the same agent.yaml deploys anywhere.

ConnectAgent Tools→ RunAgent Runtime→ BuildAgent Studio→ ShipDeploy
LayerPackageContains
Corealgen-agent-runtimeContracts, @tool, presets, Toolkit, plugins, describe, the MCP and HTTP adapters.
Connectorsalgen-agent-toolsGeneric toolkits for databases, email, chat, documents, webhooks and MCP servers, one optional extra each.
Domain packsTheir own packagesProduct-specific tools such as hiring, HR or text-to-SQL.

Why a separate package#

Pre-release. 0.2.0a10 is an alpha. A tool's name, input schema, side effect and permission are public contract; any change ships with a CHANGELOG entry and an upgrade note.

Get started

Install#

Install the extra for each connector you use. The package pulls in a supported algen-agent-runtime.

zsh
$ python -m pip install --pre 'algen-agent-tools[postgres]'

Extras#

ExtraConnectorInstalls
postgresPostgreSQLasyncpg
mysqlMySQL / MariaDBaiomysql[rsa] (with cryptography, for MySQL 8's default authentication)
sqliteSQLitenothing extra (standard library)
smtpSMTPnothing extra (standard library)
gmailGmailnothing extra (uses the Runtime's HTTP client)
outlookOutlook (Microsoft 365)nothing extra (uses the Runtime's HTTP client)
imapIMAPnothing extra (standard library)
telegramTelegramnothing extra (uses the Runtime's HTTP client)
discordDiscordnothing extra (uses the Runtime's HTTP client)
githubGitHub (MCP overlay)the Runtime's mcp extra
documentsDocumentsPyMuPDF 1.x, for PDFs (AGPL-3.0); every other format needs nothing
msgDocuments (Outlook .msg)olefile (BSD), for Outlook .msg files; Tools parses the message itself
ocrDocuments (OCR)pytesseract and Pillow, for TesseractRecognizer; the host installs the Tesseract program and its languages
webhookWebhooknothing extra (uses the Runtime's HTTP client)
mcpMCP overlaysthe Runtime's mcp extra
alleverything aboveincludes PyMuPDF (AGPL-3.0) through documents; install only the extras you use if you cannot ship AGPL code

Each connector imports its dependency only when its client first opens. If the extra is missing, the error names it, for example pip install "algen-agent-tools[postgres]".

Get started

Quickstart: add a connector to an agent#

No code is needed for the built-in connectors. Name the plugin, trust the package, enable the tools and grant their permissions.

  1. Declare the plugin in agent.yaml
    agent.yaml
    agents:
      - name: assistant
        version: 1.0.0
        description: Checks that tools are wired up
        system_instructions: Use sample.echo when asked to echo text.
        default_model: {name: m, provider: demo}
        domain_plugins: [algen_agent_tools.sample]   # the connector's plugin
        enabled_tools: [sample.echo]                  # the tools this agent may call
        tool_permissions: [sample.read]               # and their permissions
    security:
      trusted_plugin_prefixes: [algen_agent_tools]
  2. Validate it the way Studio will
    zsh
    $ algen-agent-runtime check agent.yaml
    $ algen-agent-runtime describe --config agent.yaml --json

    The first command fails on an unknown tool name and suggests the closest match. describe lists each tool with its side effect, permissions, timeout and the environment variables it needs.

  3. Set the connector's environment variables

    Each connector reads its settings from env:// references when its client first opens, never at import. See Secrets and environment.

In Studio, use Add connector instead of writing this by hand. The YAML above is what it writes.

Get started

In Studio: add a connector#

When your Studio offers the connector catalog, open the project's Connections tab, go to Tools & knowledge → Connectors, and choose Add on a connector. You can also ask azAI ("let the assistant read my Gmail"); it adds the connector the same way and asks first.

  1. Choose the agents and tools. The connector's default tools are selected. The preview shows what changes: the plugin, the tools and only the permissions they need, the trusted algen_agent_tools prefix, and the sites the connector calls (added to the allowed HTTP hosts, where you can see them).
  2. Fill in its settings. Secrets are saved encrypted and never shown again. A database connector's server must also be declared under Organization → Database hosts before hosted runs reach it.
  3. Build, if Studio says so. Until a build describes the connector, its tools show as Pending build. Where Studio runs catalogued connectors on its own image, no build is needed.

Each connector card shows its maturity (alpha connectors carry a badge), its tools with their side effects, and options only code can set. SMTP cannot run on hosted Studio yet; run it from a local Studio or a deployment. When Studio moves to a newer Tools version, the Connectors section offers Review upgrade, which lists removed tools, new permissions and new settings before anything changes.

Studio's Connectors list comes from the connector catalog of the Tools version Studio ships with, which can be older than the one this page documents. A connector added in a later release appears there once Studio moves to that release.

Get started

How it works#

  • A connector is a Runtime Toolkit. Its tools are named <toolkit>.<action> and their permissions sit under the toolkit's prefix (postgres.read, smtp.write).
  • Governance comes from a Runtime preset. read, write, external, destructive or long_running sets the side effect, idempotency, retries, timeout and approval default. Connectors never hand-set them.
  • A plugin registers it. Each connector ships a plugin in the algen_agent_runtime.plugins entry-point group under the trusted prefix algen_agent_tools. Registering opens nothing and resolves no secret.
  • The client opens late. The toolkit's client opens at container start or on first use, resolves its env:// secrets then, and is closed with the container.
Get started

What's new#

0.2.0a10 · 5 November 2026#

  • Outlook .msg files. With the new msg extra (olefile), documents.extract_text reads .msg files like .eml, with the same limits; a message whose only body is RTF reports document.body_unread. See Email files.
  • Quoted history. include="latest" on get_message and get_thread (Gmail, Outlook and IMAP) returns the sender's own text without the quoted history or signature, and sets quoted_text_removed. Gmail and IMAP bodies join every inline text part (at most five). See Quoted history.
  • Content over names. A file whose content names another type is read as what it is: a JPEG named .pdf, a .docx renamed .doc. Text, CSV, TSV, Markdown and HTML fall back from UTF-8 to Windows-1252, and read UTF-16 with a byte-order mark.
  • OpenDocument and RTF. ODT, ODS and ODP, and RTF, with the Office rules for hidden content. An embedded OLE object in RTF is the new security finding document.embedded_object. See OpenDocument and RTF.
  • Changed: get_thread defaults to include="latest". Each message's body is the sender's own text; pass include="full" for whole bodies, as 0.2.0a9 returned.
  • Changed: long PDFs are read, not refused. A PDF over 50 pages is read up to page 50 and reports document.pages_skipped; the result is partial and truncated.
  • Requires algen-agent-runtime>=0.1.0a23,<0.2 (tested on 0.1.0a23).

0.2.0a9 · 5 November 2026#

  • Whole threads. get_thread on Gmail, Outlook and IMAP returns a thread's messages oldest first, at most 50, keeping the newest. get_message results gain message_id_header, in_reply_to and references.
  • An attachment's text in one call. Built with documents=DocumentReader(), the Gmail, Outlook and IMAP toolkits offer read_attachment: it downloads the attachment, checks it and returns its inspected text, so the file never passes through the model. See Reading attachments.
  • Email files. documents.extract_text reads .eml messages with their attachments; results gain attachments, and a finding in an attachment names it in part.
  • IMAP search and folders. imap.list_messages takes text_contains and, on Gmail, gmail_query; the folders setting accepts special-use names such as \Sent and \All; imap.list_folders lists the readable folders. On a plain IMAP server thread_id is no longer empty.
  • Requires algen-agent-runtime>=0.1.0a23,<0.2 (tested on 0.1.0a23).

0.2.0a8 · 5 November 2026#

  • Unread is not risky. A scanned PDF, an image-only DOCX, a cover note over a scan or an XLSX past the row limit is now pass with coverage none or partial, where it was review. Code that treated review as "not fully read" checks coverage != "full" (or needs_ocr) instead; see Unread content.
  • OCR, when you want it. Pass a TextRecognizer to documents_toolkit(ocr=...) or extractor_for to read images and scanned PDF pages. The new ocr extra ships TesseractRecognizer; OCR is never on by default. See OCR.
  • Known limitations. Images inside DOCX and PPTX are not recognized, and a recognizer still running when the 120 s timeout fires finishes in its worker thread.
  • Requires algen-agent-runtime>=0.1.0a23,<0.2 (tested on 0.1.0a23). Runtime 0.1.0a22 and 0.1.0a23 fix two ways an approval-required workflow tool step could run without its approval; no tool changes are needed.

0.2.0a7 · 31 October 2026#

  • Refusals are not "may have happened". A refusal before anything is sent (a token that could not be fetched, an unconfigured host, an attachment refused or over the limit, invalid MCP arguments, a too-large webhook payload) and a provider's definite rejection (a 4xx other than 408 from Graph, Gmail, Telegram, Discord or a webhook receiver; an SMTP connection, login, sender, recipient or data refusal) are recorded as not attempted, so a retry runs instead of manual reconciliation. Timeouts, dropped connections and 5xx stay indeterminate. Errors keep their type and message.
  • Reply to what was read. Gmail, Outlook and IMAP results carry sender_address, sender_name, recipient_addresses and cc_addresses, declared format: email, so the default pii guardrail keeps them; see Email and guardrails.
  • Document inspection lives here. The extractors moved from the Runtime to algen_agent_tools.documents (extractor_for, ExtractedDocument, …); the documents extra installs PyMuPDF directly. DOCX and PPTX with text and images now return review. runtime_attachment_loader reads the run's own attachments; see Documents.
  • Fixed. An MCP overlay session closed from another task no longer fails; OAuthRefresh uses a rotated refresh token, and on_refresh_token= on the Gmail and Outlook clients stores it.
  • Requires algen-agent-runtime>=0.1.0a20,<0.2 (tested on 0.1.0a20 and 0.1.0a21). Runtime 0.1.0a19 fixed the session-scoped tool cache.

0.2.0a6 · 30 October 2026#

  • The caller's rows. context_params on query() for PostgreSQL, MySQL and SQLite binds the call's tenant_id, user_id, run_id or step_id, which the model cannot choose. The tool declares them as its cache_context, so the Runtime's tool-result cache never serves one caller's rows to another. See The caller's rows.
  • More document formats. documents.extract_text reads Excel (XLSX), PowerPoint (PPTX), HTML, CSV and TSV as well as PDF, DOCX, text and Markdown, with hidden sheets, slides, shapes and HTML elements reported rather than returned. truncated is also true when the inspection cut the text.
  • Requires algen-agent-runtime>=0.1.0a18,<0.2. Don't scope the tool-result cache to session: on Runtime 0.1.0a18 it fails every cacheable tool call.

0.2.0a5 · 28 October 2026#

  • IMAP: read one mailbox with an app password, read-only (nothing is marked as seen), with typed filters instead of raw IMAP search.
  • sender_auth (pass, fail or unknown) on inbound mail: the receiving server's SPF, DKIM and DMARC verdict, filled by Gmail and IMAP.

0.2.0a4 · 27 October 2026#

  • MySQL / MariaDB and SQLite on a shared SQL core, with the same typed queries as PostgreSQL.
  • GitHub: the first shipped MCP overlay, 37 reviewed tools on GitHub's hosted MCP server.
  • Webhook: typed, signed events to URLs the deployment fixes.

0.2.0a3 · 27 October 2026#

  • Telegram and Discord bots on one shared messaging toolkit, limited to an allowlist of conversations.
  • python -m algen_agent_tools.mcp check <overlay> reports drift between an overlay and its live server.

0.2.0a2 · 27 October 2026#

  • Outlook (Microsoft 365) through Microsoft Graph, delegated or application access.
  • gmail.get_message, and get_attachment for Gmail and Outlook when the toolkit has an artifact sink.
  • Documents: documents.extract_text with the Runtime's document inspection.

0.2.0a1 · 27 October 2026#

  • The connector catalog, connectors.json, shipped in the wheel: every connector's settings, egress hosts and tool contracts, which Studio reads to list and enable connectors without running their code.
  • Shared attachment handling (AttachmentPolicy, artifact loaders and sinks), and ALGEN_TOOLS_SMTP_ALLOWED_DOMAINS / ALGEN_TOOLS_GMAIL_ALLOWED_DOMAINS to bound recipients without code.

0.1.0a2 · 27 October 2026#

  • Email under the default guardrails. Address fields in smtp.send_email and gmail.send_email declare format: email, so the Runtime's pii guardrail leaves recipients alone and still screens the rest of the message. Agents no longer need a tool_policies setting to send; see Email and guardrails.
  • Requires algen-agent-runtime>=0.1.0a12,<0.2.

0.1.0a1 · 27 October 2026#

  • First release, supporting algen-agent-runtime>=0.1.0a11,<0.2.
  • Connectors: PostgreSQL (typed queries and catalog reads), SMTP and Gmail (governed sending, inbox listing), and the dependency-free sample toolkit.
  • MCP governance overlays with schema pinning, deny-by-default and a snapshot command.
  • Contract harness: assert_toolkit_contract, describe_plugin, and a fake for every connector.
  • Replaces the Runtime's deprecated communications email tools and database_tool; see Migrating from Runtime.
Connectors

PostgreSQL#

Connect an agent to one PostgreSQL (Postgres) database through typed actions. The model never writes SQL: each tool is a named, parameterised query a developer declares in code, and the model fills in its typed parameters.

Pluginalgen_agent_tools.postgres
Extrapostgres (asyncpg)
EnvironmentALGEN_TOOLS_POSTGRES_DSN

Zero-configuration tools#

ToolSide effectPermissionReturns
postgres.list_tablesreadpostgres.readTables and views in the public schema with approximate row counts.
postgres.describe_tablereadpostgres.readColumns: type, nullability, default, primary key.

Declaring typed queries#

Most agents need the database's own actions. Declare them in your project's plugin:

plugin.py
from pydantic import BaseModel, Field
from algen_agent_runtime.tools.authoring import write
from algen_agent_tools.postgres import PostgresToolkit

class CustomerInvoices(BaseModel):
    customer_id: str = Field(min_length=1)
    due_before: date

billing = PostgresToolkit("billing", dsn="env://BILLING_DATABASE_URL")

billing.query(
    "list_open_invoices",
    "SELECT id, amount, due FROM invoices "
    "WHERE customer_id = $1 AND status = 'open' AND due < $2 ORDER BY due",
    description="List one customer's open invoices that fall due before a date.",
    params=CustomerInvoices,
    row=Invoice,
)

billing.query(
    "add_note",
    "INSERT INTO invoice_notes (invoice_id, text, request_key) VALUES ($1, $2, $3) "
    "ON CONFLICT (request_key) DO NOTHING",
    description="Add a note to an invoice.",
    params=NewNote,
    preset=write(),
    idempotency_key=True,   # the call's key is bound as the last parameter
)
  • params fields fill $1, $2, … in field order; a query whose placeholders do not match is refused when it is declared. Unknown input fields are rejected.
  • A read returns list[row]. A write returns list[row] with RETURNING, otherwise {"rows_affected": n}.
  • preset is any Runtime preset: read() (default), write(approval=...), destructive() (approval on by default), long_running(...).
  • postgres_toolkit(name, dsn=..., schemas=(...)) builds a toolkit that already has the two catalog reads, limited to the schemas you list.

The caller's rows#

In a shared, multi-tenant database, bind the caller's identity with context_params instead of asking the model for it:

plugin.py
billing.query(
    "my_invoices",
    "SELECT id, amount, due FROM invoices "
    "WHERE due < $1 AND tenant_id = $2 AND owner_id = $3 ORDER BY due",
    description="List the signed-in user's invoices that fall due before a date.",
    params=DueBefore,                         # $1, from the model
    row=Invoice,
    context_params=("tenant_id", "user_id"),  # $2, $3, from the call
)
  • The values are ToolContext.tenant_id, user_id, run_id and step_id, which the Runtime sets from the run's authenticated request. Nothing else can be bound.
  • They are not in the tool's input schema, and a call that sends one is rejected. A params field whose name or alias is a context value is refused when the query is declared.
  • Placeholders fill in this order: params fields, then context_params in the order given, then the idempotency key.
  • Caching: the tool declares the bound values as its cache_context, so the Runtime's tool-result cache keys each result on them and never serves one caller's rows to another, at any cache scope. Don't use the session scope for tool results: on Runtime 0.1.0a18 it fails every cacheable call.
  • They narrow rows; they don't replace privileges. Keep a least-privilege role, and use row-level security where the database has it.

The same argument works for MySQL (%s) and SQLite (?) toolkits: all three share algen_agent_tools.sql, and each database adds only a dialect for its placeholders, read-only reads, transactions and timeouts.

Guarantees#

  • No SQL from the model. Inputs are typed fields; values are always bound parameters.
  • Reads cannot write. A read runs in a READ ONLY transaction, so PostgreSQL itself refuses an UPDATE hidden in one.
  • Bounded. Every statement runs under SET LOCAL statement_timeout equal to the tool's timeout. Reads stream through a cursor and fail with TooManyRows, rather than silently truncate, past the row limit (500 by default).
  • Writes are keyed and tried once. With idempotency_key=True the SQL can be exactly-once.
  • The caller's identity comes from the call. context_params values are never tool input, and the result cache keys on them.
  • Catalog reads are limited to the schemas you list and to tables the role can SELECT.

Connect with a database role that has only the privileges the declared queries need.

Connectors

MySQL / MariaDB#

Read a MySQL or MariaDB database through typed tools. Your own queries are declared in code exactly as for PostgreSQL, with %s placeholders (a literal % is %%).

Pluginalgen_agent_tools.mysql
Extramysql (aiomysql[rsa])
EnvironmentALGEN_TOOLS_MYSQL_DSN (secret): mysql://user:password@host:port/database, with ?ssl=verify for verified TLS
ToolSide effectPermissionTimeoutAttemptsApproval
mysql.list_tablesreadmysql.read30 sRuntime defaultagent policy
mysql.describe_tablereadmysql.read30 sRuntime defaultagent policy
  • Reads run in START TRANSACTION READ ONLY, so a declared read that writes fails.
  • Timeouts are enforced three ways: on the server (max_execution_time on MySQL, max_statement_time on MariaDB, detected at connect), on lock waits (innodb_lock_wait_timeout), and by a client-side deadline that drops a stuck connection.
  • Writes run in their own transaction. With idempotency_key=True, INSERT IGNORE or ON DUPLICATE KEY UPDATE on a unique key makes a write exactly-once.
  • Errors carry only the MySQL error number, never the server's message, which can quote data.
Connectors

SQLite#

Read a SQLite database file through typed tools, with your own queries declared in code using plain ? placeholders. Numbered and named placeholders are refused, so the order can't differ from the model's fields.

Pluginalgen_agent_tools.sqlite
Extrasqlite (no extra packages)
EnvironmentALGEN_TOOLS_SQLITE_PATH
ToolSide effectPermissionTimeoutAttemptsApproval
sqlite.list_tablesreadsqlite.read30 sRuntime defaultagent policy
sqlite.describe_tablereadsqlite.read30 sRuntime defaultagent policy
  • Read-only by default. The file opens with mode=ro, and declaring a write fails unless the toolkit is built with read_only=False. Reads run with PRAGMA query_only.
  • Timeouts: SQLite has no statement timeout, so a progress handler interrupts a statement that runs past the tool's timeout.
  • The file must already exist; the connector never creates one. SQLite suits small, local, read-mostly data. For concurrent writers, use PostgreSQL.
Connectors

SMTP#

Send email over SMTP from the deployment's own address.

Pluginalgen_agent_tools.smtp
Extrasmtp (no extra packages)
EnvironmentALGEN_TOOLS_SMTP_HOST, ALGEN_TOOLS_SMTP_SENDER (required); ALGEN_TOOLS_SMTP_PORT (587), ALGEN_TOOLS_SMTP_SECURITY (starttls), ALGEN_TOOLS_SMTP_USERNAME, ALGEN_TOOLS_SMTP_PASSWORD
ToolSide effectPermissionTimeoutAttemptsApproval
smtp.send_emailexternalsmtp.write60 s1agent policy (asks by default)

smtp.send_email takes to (1 to 50), cc, bcc, reply_to, subject, and text_body and/or html_body. It returns the delivery receipt: provider, Message-ID, accepted and rejected recipients.

Sending guarantees (SMTP and Gmail)#

  • The sender is the deployment's. The model cannot choose the From address or add headers.
  • Header-safe. Line breaks in addresses, names or the subject are refused; recipients must be unique, at most 100 in all.
  • One message per idempotency key. The key becomes a deterministic Message-ID and the tool is tried once, so a replay does not send twice.
  • Bounded audience. With allowed_domains, every recipient and the Reply-To address must be in those domains or their subdomains.
  • TLS by default. security is starttls or tls; plain is allowed only to loopback.
  • Content is not captured in audit metadata, and errors name only the failure type.

Attachments are offered only when you configure an artifact loader; see Configure in code.

Connectors

Gmail#

Read and send mail for one Gmail or Google Workspace mailbox, through the Gmail API with an OAuth refresh token.

Pluginalgen_agent_tools.gmail
Extragmail (no extra packages)
EnvironmentALGEN_TOOLS_GMAIL_CLIENT_ID, ALGEN_TOOLS_GMAIL_CLIENT_SECRET, ALGEN_TOOLS_GMAIL_REFRESH_TOKEN, ALGEN_TOOLS_GMAIL_SENDER
ToolSide effectPermissionTimeoutAttemptsApproval
gmail.list_messagesreadgmail.read120 sRuntime defaultagent policy
gmail.get_messagereadgmail.read60 sRuntime defaultagent policy
gmail.get_threadreadgmail.read120 sRuntime defaultagent policy
gmail.get_attachment (with artifact_sink)readgmail.read120 sRuntime defaultagent policy
gmail.read_attachment (with documents)readgmail.read120 sRuntime defaultagent policy
gmail.send_emailexternalgmail.write60 s1agent policy (asks by default)
  • gmail.list_messages(query="is:unread", max_results=25, page_token=None) takes a Gmail search query and returns message metadata: sender, recipients, subject, a snippet, labels, attachments with their part_id, and sender_auth, at most 100 per page. Bodies and attachment bytes are not returned. The query reaches every folder: in:sent lists sent mail, in:anywhere includes archived mail, spam and trash.
  • sender_auth is Gmail's own SPF, DKIM and DMARC verdict on the sender (pass, fail or unknown), read only from the Authentication-Results header Gmail stamped. Use it to keep a spoofed From out of trusted workflows; unknown is not a failure.
  • gmail.get_message(message_id, max_chars=20000, include="full") reads one message with its body as plain text (HTML without markup, scripts or styles; include="latest" keeps only the sender's own text), with body_truncated set when cut. The tool tells the model to treat instructions in the message as data. Results carry message_id_header, in_reply_to and references.
  • gmail.get_thread(thread_id, max_messages=20, max_chars_per_message=4000) reads a whole thread, oldest first, each message as get_message returns it with the body cut to max_chars_per_message (at most 10,000). At most 50 messages; a longer thread keeps its newest and sets truncated. By default (include="latest") each body is the sender's own text; see Quoted history.
  • gmail.get_attachment(message_id, attachment_id) is offered only when the toolkit is built in code with an artifact_sink. It saves one attachment as an artifact, within the attachment_policy size and type checks, and returns only its reference. Pass the stable part_id from the listing.
  • gmail.read_attachment(message_id, attachment_id, max_chars=50000) is offered only when the toolkit is built with documents; see Reading attachments.
  • gmail.send_email takes the same input as smtp.send_email, sends from ALGEN_TOOLS_GMAIL_SENDER, and delivers Bcc recipients without exposing them to other recipients.

Quoted history#

From 0.2.0a10, include="latest" on get_message (default "full") and get_thread (default "latest") returns only what the sender wrote: the history the reply quotes and the signature are removed, and quoted_text_removed is set. The rules are deterministic and conservative, so text is removed only after a line that clearly starts quoted history:

  • an attribution line, On <date>, <name> wrote:, also wrapped over two lines and in French, German, Spanish, Italian and Dutch;
  • an Outlook header block (From: then Sent:/Date: and To:/Subject:), or -----Original Message-----;
  • a run of > lines with nothing but blank lines after it (inline answers between quoted lines are kept);
  • a signature after the standard -- line, or a final Sent from my … line.

A forwarded message is new content and is always kept, and a message that is only a quote is returned whole. Gmail and IMAP bodies join every inline text part (at most five), so text a client split around inline images is not lost. The same applies to the Outlook and IMAP toolkits.

Credentials#

Create an OAuth client in Google Cloud, authorise the mailbox once to obtain a refresh token, and grant only the scopes the agent needs: gmail.readonly for list_messages, get_message, get_thread, get_attachment and read_attachment, gmail.send for send_email. The access token is refreshed in memory and never leaves the client; token and API errors name only the failure type.

Reading attachments#

documents=DocumentReader() adds read_attachment: it downloads one attachment, checks it against attachment_policy and reads it with the Documents inspection, so the model gets the attachment's text in one call instead of get_attachment then documents.extract_text. The result is the same as documents.extract_text (text, disposition, findings, coverage, attachments for an attached email) plus artifact; a quarantined attachment's text is withheld. With an artifact_sink as well, the file is also saved and its reference returned, for example so a person can review a quarantined file. DocumentReader(max_bytes=..., quarantine=..., ocr=...) takes the documents toolkit's options. The same option exists on the Outlook and IMAP toolkits.

plugin.py
from algen_agent_tools.documents import DocumentReader
from algen_agent_tools.gmail import gmail_toolkit

gmail = gmail_toolkit(documents=DocumentReader())  # adds gmail.read_attachment; PDFs need the documents extra
Connectors

Outlook (Microsoft 365)#

Read and send mail for one Outlook mailbox through Microsoft Graph, with an Entra app registration.

Pluginalgen_agent_tools.outlook
Extraoutlook (no extra packages)
EnvironmentALGEN_TOOLS_OUTLOOK_TENANT_ID, ALGEN_TOOLS_OUTLOOK_CLIENT_ID, ALGEN_TOOLS_OUTLOOK_CLIENT_SECRET, ALGEN_TOOLS_OUTLOOK_MAILBOX; optional ALGEN_TOOLS_OUTLOOK_REFRESH_TOKEN, ALGEN_TOOLS_OUTLOOK_ALLOWED_DOMAINS
ToolSide effectPermissionTimeoutAttemptsApproval
outlook.list_messagesreadoutlook.read120 sRuntime defaultagent policy
outlook.get_messagereadoutlook.read60 sRuntime defaultagent policy
outlook.get_threadreadoutlook.read120 sRuntime defaultagent policy
outlook.get_attachment (with artifact_sink)readoutlook.read120 sRuntime defaultagent policy
outlook.read_attachment (with documents)readoutlook.read120 sRuntime defaultagent policy
outlook.send_emailexternaloutlook.write60 s1agent policy (asks by default)
  • outlook.list_messages(folder="inbox", search=None, unread_only=False, max_results=25, page_token=None) lists one folder (inbox, sentitems, drafts, archive, junkemail), newest first, at most 50 per page. The page token is opaque and never a URL; the connector rebuilds each request for its own mailbox.
  • get_message and get_attachment are the same shared tools as Gmail's, including include="full"|"latest" on get_message. Only file attachments can be saved.
  • outlook.get_thread(thread_id, max_messages=20, max_chars_per_message=4000) reads a conversation (conversationId) across the mailbox's folders, as for Gmail, with each body the sender's own text unless include="full" (see Quoted history). Graph returns at most 50 of its messages in one call; truncated says when there were more. In-Reply-To and References come from the message's internet headers, which Graph keeps only for received mail.
  • outlook.read_attachment(message_id, attachment_id, max_chars=50000), with documents=DocumentReader(), is the shared tool: an attachment's inspected text in one call; see Reading attachments.
  • outlook.send_email takes the same input as smtp.send_email and saves a copy in Sent Items. Attachments travel inline, at most 3 MB in all.
  • Delegated access (a refresh token is set) acts as the signed-in user. Application access uses the app's own credentials on ALGEN_TOOLS_OUTLOOK_MAILBOX.

Application access: limit the app to the one mailbox with an Exchange Online application access policy (or RBAC for Applications). Otherwise the app's Mail.Read grant reaches every mailbox in the tenant.

Connectors

IMAP#

Read one mailbox over IMAP with an app password: Gmail and Google Workspace without an OAuth app, Zoho, Fastmail or any IMAPS server. The connector is read-only: nothing is moved, flagged, deleted or marked as seen. Send replies with smtp.send_email.

Pluginalgen_agent_tools.imap
Extraimap (no extra packages)
EnvironmentALGEN_TOOLS_IMAP_HOST, ALGEN_TOOLS_IMAP_USERNAME, ALGEN_TOOLS_IMAP_PASSWORD; optional ALGEN_TOOLS_IMAP_PORT (993), ALGEN_TOOLS_IMAP_FOLDERS (INBOX), ALGEN_TOOLS_IMAP_AUTHSERV_ID
ToolSide effectPermissionTimeoutAttemptsApproval
imap.list_messagesreadimap.read120 sRuntime defaultagent policy
imap.list_foldersreadimap.read60 sRuntime defaultagent policy
imap.get_messagereadimap.read60 sRuntime defaultagent policy
imap.get_threadreadimap.read120 sRuntime defaultagent policy
imap.get_attachment (with artifact_sink)readimap.read120 sRuntime defaultagent policy
imap.read_attachment (with documents)readimap.read120 sRuntime defaultagent policy
  • imap.list_messages takes typed filters, not raw IMAP search: from_address, subject, since, before, unread_only and text_contains (text anywhere in the headers or body, IMAP TEXT), at most 100 per page, newest first. Search text is printable ASCII; anything else is refused rather than sent.
  • gmail_query on imap.list_messages is a Gmail search (X-GM-RAW, for example has:attachment newer_than:7d) and is refused unless the server is Gmail.
  • Only the folders in ALGEN_TOOLS_IMAP_FOLDERS can be read. Folders open with EXAMINE and bodies are fetched with BODY.PEEK, by part and within byte limits.
  • Special-use folders. An entry in ALGEN_TOOLS_IMAP_FOLDERS may be \Sent, \All, \Archive, \Drafts, \Junk, \Trash or \Flagged, resolved with LIST when the connection opens, so INBOX,\Sent,\All reads the inbox, sent mail and, on Gmail, All Mail, whatever the server calls them. A name the server does not mark is left out. On Gmail, \All also lets get_message find a message by its id after it has left the inbox.
  • imap.list_folders() lists the folders the tools may read, each with its special use when the server marks one, never a folder outside ALGEN_TOOLS_IMAP_FOLDERS.
  • On Gmail, message and thread ids are Gmail's own, the same ones the Gmail API returns. Elsewhere an id carries the folder's UIDVALIDITY, so a stale id is an error rather than another message, and thread_id (empty before 0.2.0a9) is r.<base64url> of the thread's first Message-ID.
  • imap.get_thread(thread_id, max_messages=20, max_chars_per_message=4000) reads a thread across the configured folders: by X-GM-THRID on Gmail, by the References chain elsewhere. Add \Sent to the folders to see the mailbox's own replies.
  • imap.get_message and imap.get_thread take include as on Gmail: "full" by default for get_message, "latest" for get_thread. Bodies join every inline text part (at most five); see Quoted history.
  • imap.read_attachment(message_id, attachment_id, max_chars=50000), with documents=DocumentReader(), returns an attachment's inspected text in one call; see Reading attachments.
  • sender_auth trusts the topmost Authentication-Results header only from mx.google.com on Gmail, or from the server named in ALGEN_TOOLS_IMAP_AUTHSERV_ID; otherwise it is unknown.
  • TLS on port 993 is always on, with the certificate and host name verified. The password is sent only in LOGIN.
Connectors

Telegram#

Read the messages a Telegram bot receives, save their files as artifacts, and post plain text as the bot, through the Bot API.

Pluginalgen_agent_tools.telegram
Extratelegram (no extra packages)
EnvironmentALGEN_TOOLS_TELEGRAM_BOT_TOKEN; optional ALGEN_TOOLS_TELEGRAM_ALLOWED_CHATS
ToolSide effectPermissionTimeoutAttemptsApproval
telegram.list_messagesreadtelegram.read60 sRuntime defaultagent policy
telegram.get_attachment (with artifact_sink)readtelegram.read120 sRuntime defaultagent policy
telegram.send_messageexternaltelegram.write60 s1agent policy (asks by default)
  • telegram.list_messages returns the bot's pending updates, newest first, at most 100, without confirming them, so reading changes nothing. Telegram keeps an update for 24 hours.
  • telegram.send_message posts up to 4,096 characters of plain text with link previews off, so a link can't make Telegram fetch a URL.
  • Chat ids are numbers; @username is not accepted. The allowlist limits every tool to those chats.
  • The bot token is part of every Bot API URL; it never appears in an error and is redacted from request logs.

Telegram and Discord share algen_agent_tools.messaging: the same models (InboundMessage, Conversation, DeliveryReceipt), tool names, inputs and conversation allowlist. A platform declares its capabilities, and messaging_toolkit adds only the tools it supports.

Reading uses getUpdates, which fails with error 409 while the bot has a webhook or another program polls it. Use a bot for this connector alone.

Connectors

Discord#

List one Discord server's text channels, read their messages, save attachments as artifacts, and post plain text as a bot, through the REST API.

Pluginalgen_agent_tools.discord
Extradiscord (no extra packages)
EnvironmentALGEN_TOOLS_DISCORD_BOT_TOKEN, ALGEN_TOOLS_DISCORD_GUILD_ID; optional ALGEN_TOOLS_DISCORD_ALLOWED_CHANNELS
ToolSide effectPermissionTimeoutAttemptsApproval
discord.list_conversationsreaddiscord.read60 sRuntime defaultagent policy
discord.list_messagesreaddiscord.read60 sRuntime defaultagent policy
discord.get_attachment (with artifact_sink)readdiscord.read120 sRuntime defaultagent policy
discord.send_messageexternaldiscord.write60 s1agent policy (asks by default)
  • One server. Every channel is checked to be a text channel, announcement channel or thread of ALGEN_TOOLS_DISCORD_GUILD_ID; direct messages, voice channels and other servers are refused.
  • discord.send_message posts up to 2,000 characters with no pings (@everyone, roles and users don't notify anyone), no link embeds, and an enforced nonce from the idempotency key, so Discord drops a repeated send.
  • The token travels only in the Authorization header to discord.com, never to the CDN.
  • To read message text, turn on the bot's Message Content privileged intent.

Telegram and Discord share algen_agent_tools.messaging: the same models (InboundMessage, Conversation, DeliveryReceipt), tool names, inputs and conversation allowlist. A platform declares its capabilities, and messaging_toolkit adds only the tools it supports.

Connectors

GitHub#

Read repositories, commits, issues and pull requests, and work on issues and pull requests, through GitHub's hosted MCP server, governed by a reviewed MCP overlay that ships with the package.

Pluginalgen_agent_tools.github
Extragithub (the Runtime's MCP client)
EnvironmentALGEN_TOOLS_GITHUB_TOKEN
Serverhttps://api.githubcopilot.com/mcp/, the only host the connector reaches

It declares 37 of the server's 46 tools as github.<tool>, with github.read for reads and github.write for everything else. Each input schema is pinned to what the server advertised when the overlay was reviewed; a changed tool is refused until it is reviewed again.

KindToolsApproval
Read (22)get_me, get_file_contents, get_commit, list_commits, list_branches, list_tags, get_tag, list_releases, get_latest_release, get_release_by_tag, issue_read, list_issues, search_issues, list_issue_types, list_issue_fields, get_label, pull_request_read, list_pull_requests, search_pull_requests, search_code, search_repositories, search_commitsagent policy
Write (10)add_issue_comment, update_issue_comment, issue_write, sub_issue_write, create_branch, create_pull_request, update_pull_request, update_pull_request_branch, add_reply_to_pull_request_comment, add_comment_to_pending_reviewagent policy
Write, approved every call (3)pull_request_review_write (it can approve a pull request), push_files and create_or_update_file (they commit to a branch)always
Destructive (2)merge_pull_request, delete_fileby default

Not exposed: repository creation and forks, run_secret_scanning (it uploads the content it is given), the MCP-Apps UI tool, Copilot review requests, and people-directory lookups.

Use a fine-grained personal access token (or a GitHub App user token) limited to the repositories the agent works on, with only the permissions the enabled tools need. The overlay limits which tools an agent can call; the token limits what they can reach. Issue and pull request text comes from outside contributors, so keep write tools behind approval when an agent reads and acts in one run.

Connectors

Documents#

Read the text of a document an agent received, for example an email attachment saved with gmail.get_attachment. Parsing and inspection are the untrusted-document checks in algen_agent_tools.documents (moved from the Runtime in 0.2.0a7); the connector reads the artifact for the calling tenant, bounds the call and governs it as a tool.

Modulealgen_agent_tools.documents (built in code; it needs an artifact loader, the deployment's or runtime_attachment_loader for the run's attachments, so there is no plugin yet)
Extradocuments for PDFs (PyMuPDF, AGPL-3.0); every other format needs nothing. Optional ocr for Tesseract OCR and msg for Outlook .msg files (olefile, BSD)
Environmentnone
ToolSide effectPermissionTimeoutAttemptsApproval
documents.extract_textreaddocuments.read120 sRuntime defaultagent policy
  • Formats come from the file name: .pdf, .docx, .xlsx, .pptx, .odt, .ods, .odp, .rtf, .html, .htm, .csv, .tsv, .txt, .md, .eml and .msg, and images with a recognizer (see OCR). Any other suffix, or content that doesn't match its name (a renamed executable), is refused.
  • Content wins over the name. From 0.2.0a10, when the content's own signature names another supported type, it is read as that type: a scan.pdf that is really a JPEG as an image (with OCR), a .docx renamed .doc as Word, a PDF named .txt as a PDF. Text formats have no signature and go by their name; text, CSV, TSV, Markdown and HTML are read as UTF-8, as UTF-16 when they start with its byte-order mark, and otherwise as Windows-1252, the encoding Excel uses for CSV exports.
  • Long PDFs are read up to page 50 and report document.pages_skipped, so the result is partial and truncated; before 0.2.0a10 they were refused.
  • Only visible text is returned, cut to max_chars (at most 100,000). Hidden PDF text, hidden DOCX runs, hidden XLSX sheets, rows and columns, hidden PPTX slides and shapes, and HTML hidden by attribute or inline style are reported as findings instead.
  • A disposition (pass, review or quarantine) and findings cover hidden text, instructions aimed at the model and risky packaging (macros, remote templates and objects, and an embedded OLE object in RTF, document.embedded_object). A quarantined document's text is withheld unless the toolkit is built with quarantine="return".
  • truncated is true when the text was cut, by max_chars or by the inspection (an XLSX sheet past 5,000 rows).
  • Images are not read without OCR. A PDF, DOCX or PPTX with text and embedded images reports document.images_excluded as a coverage finding. Pass a recognizer (OCR), or attach the file to the run when a vision model should see it.
  • Inspection without the tool. from algen_agent_tools.documents import extractor_for gives the extractors directly. They replace algen_agent_runtime.security.documents, deprecated in Runtime 0.1.0a19.
plugin.py
from algen_agent_tools.documents import documents_toolkit

documents = documents_toolkit(artifact_loader=load_artifact)  # checks the artifact's tenant

# or, for the files attached to the run (Runtime 0.1.0a19+): refuses any other artifact
from algen_agent_tools.attachments import runtime_attachment_loader
documents = documents_toolkit(artifact_loader=runtime_attachment_loader)

Unread content#

From 0.2.0a8, content the inspection did not read is reported apart from security findings, so a caller can tell "dangerous" from "not read":

  • coverage is full (everything was read), partial (some content was not) or none (content was found but no text could be read). needs_ocr is true when images or scanned pages hold text only OCR can read.
  • Each finding has a category, security or coverage. document.ocr_required, document.images_excluded, document.ocr_no_text, document.truncated, document.attachment_skipped, document.attachment_unread, document.body_unread (an Outlook message whose only body is RTF) and document.pages_skipped (PDF pages past page 50) are coverage findings; only security findings set the disposition.
  • A scanned ID card without OCR is pass with coverage="none" and needs_ocr=true; a cover note over a scan is pass with coverage="partial". Hidden text or an instruction aimed at the model still makes it review or quarantine.

pass with coverage="none" means nothing dangerous was found because nothing was read. Before 0.2.0a8 such documents were review; code that used review to mean "not fully read" checks coverage != "full" instead.

OCR#

OCR is off unless the host passes a recognizer, a TextRecognizer: any object with recognize(image, *, media_type) returning RecognizedText. Pass it as documents_toolkit(ocr=...) or extractor_for(filename, ocr=..., ocr_max_pages=3). The ocr extra ships TesseractRecognizer, backed by the Tesseract program, which the host installs with its languages.

plugin.py
# pip install "algen-agent-tools[documents,ocr]"
from algen_agent_tools.documents import TesseractRecognizer, documents_toolkit

documents = documents_toolkit(
    artifact_loader=load_artifact, ocr=TesseractRecognizer(languages=("eng", "deu"), timeout=30)
)
  • Images (.png, .jpg, .jpeg, .webp, .gif, .bmp, .tif, .tiff) are accepted only with a recognizer, within 40 megapixels and 64 frames.
  • Scanned PDF pages (images and almost no text) are rendered and recognized, up to ocr_max_pages per PDF (default 3); pages past the limit report document.ocr_required.
  • Recognized text is checked like any other. It passes the same prompt-injection checks, so an instruction in a scan quarantines the document. An image or page with no recognized text reports document.ocr_no_text.
  • TesseractRecognizer fails when it is built, naming the fix, if the extra, the program or a language is missing. It is never the default.
  • The recognizer runs inside the tool's 120 s timeout. Images inside DOCX and PPTX are not recognized yet.

OpenDocument#

From 0.2.0a10, ODT, ODS and ODP files (LibreOffice, Google Docs exports) are read with the standard library, with the rules of their Office counterparts.

  • Hidden content is left out and reported: text a style or text:display="none" hides, hidden text fields and paragraphs, text under 2 pt, hidden sheets, rows and columns, and hidden slides.
  • Spreadsheets come out sheet by sheet under --- Sheet: <name> ---, cells joined by | , cut at 5,000 rows per sheet. Comments, tracked changes and speaker notes are not extracted.
  • A Basic macro project, or an image or object loaded from a remote address, is a finding; encrypted files are refused.

RTF#

From 0.2.0a10, RTF is read with a bounded tokenizer from the standard library: paragraphs, tabs and table cells (joined with | ), with escaped and Unicode characters decoded. Font tables, styles, document information, headers, footers and footnotes are not text.

  • Hidden text (\v) and text under 2 pt are left out and reported.
  • An embedded OLE object, a common exploit carrier, is the security finding document.embedded_object; a field that loads a remote file (INCLUDEPICTURE, INCLUDETEXT) is document.external_reference. Pictures are coverage findings.
  • A Word file saved as RTF but named .doc is recognised by its content.

Email files#

From 0.2.0a9 an .eml file is read as its headers (From, To, Cc, Date, Subject) and body, then each attachment's text under an [Attachment: <name>] line, read by the extractor for its type. Attached emails are read at most two levels deep, and at most 20 attachments in the whole message.

  • attachments lists the attachments that were read (file name, media type, disposition, coverage).
  • An attachment's findings are the message's. Hidden text or an instruction in an attachment quarantines the message, and the finding names the attachment in part.
  • Skipped is not refused. An attachment past a limit, of an unsupported type or unreadable is a coverage finding (document.attachment_skipped or document.attachment_unread), so the message is pass with coverage="partial".
  • Outlook .msg files (from 0.2.0a10, with the msg extra, pip install "algen-agent-tools[msg]") are read the same way: Tools reads the sender, recipients, date, subject, body and attachments from the file's OLE streams with olefile, rebuilds the equivalent email and inspects that, with the same limits and findings. Attached Outlook messages are read as attached emails. A message whose only body is RTF reports document.body_unread (coverage), since RTF bodies are not read.
Connectors

Webhook#

Send typed events to systems you own: chat incoming-webhooks, automation platforms, your own services. The deployment fixes where each event goes and what shape it has; the model only fills in the fields.

Modulealgen_agent_tools.webhook (built in code; there is no plugin)
Extrawebhook (no extra packages)
Environmentone env:// URL per target, and an optional signing secret per target
plugin.py
from algen_agent_tools.webhook import WebhookTarget, webhook_toolkit

alerts = webhook_toolkit([
    WebhookTarget(
        action="notify_ops",                           # tool webhook.notify_ops
        url="env://ALGEN_TOOLS_WEBHOOK_OPS_URL",
        signing_secret="env://ALGEN_TOOLS_WEBHOOK_OPS_SECRET",
        payload=OpsAlert,                              # a Pydantic model: the tool's input
        description="Post an alert to the operations channel.",
    ),
])
  • Each target is one external tool, webhook.<action>, with permission webhook.write, a 30 s timeout and one attempt; approval=True asks before every call.
  • The URL must be https (or allow_http=True for a private network), redirects are not followed, and the URL is treated as a secret: it never appears in an error or a log line.
  • The receiver gets a JSON POST with an Idempotency-Key header and, given a signing secret, X-Algen-Signature: t=<ts>,v1=<HMAC-SHA256>. Check it with verify(secret, header, raw_body).
  • The result is only {"action", "status_code"}; the receiver's body is never returned to the model.
Connectors

Sample toolkit#

A dependency-free toolkit that calls no external service. Use it to check that an install, the plugin entry point and describe work end to end, and as the template for a new connector.

ToolSide effectPermissionTimeoutInput
sample.echoreadsample.read5 stext, 1 to 1,000 characters; returns {"text", "length"}
Connectors

MCP governance overlays#

When a well-maintained MCP server already does what an agent needs, use a governance overlay instead of a native connector. An overlay is a reviewed file, kept in the project, that declares each remote tool the agent may call: its side effect, whether a person must approve every call, and the input schema it was reviewed against. MCP's own annotations (readOnlyHint, destructiveHint) are hints a server sets about itself; the overlay is the deployment's decision.

What an overlay guarantees#

  • Visible before connecting. Each declared tool is an ordinary Runtime tool, so describe and Studio list it with its side effect, permission and approval rule.
  • Deny by default. A remote tool the overlay does not declare is never exposed.
  • Schema pinning. A declared tool that disappeared, or whose input schema changed on the server, is refused until the overlay is reviewed again (on_drift: allow relaxes this).
  • Validated arguments. Arguments are checked against the reviewed schema, with every object closed to unknown fields, before anything is sent.
  • Bounded results. Each tool has a result size limit (256 KB by default) and a timeout.
  • Secrets by reference, hosts pinned. Tokens, headers and a stdio server's environment are env:// references; an HTTP transport may reach only the server's own host unless allowed_hosts says otherwise.

Draft, review, use#

zsh
$ python -m pip install --pre 'algen-agent-tools[mcp]'
$ python -m algen_agent_tools.mcp snapshot server.yaml --name github --output github.overlay.yaml

The draft pins every advertised tool's schema and leaves each effect empty, with the server's annotation as a comment. It does not load until someone sets every effect. Delete each tool the agent must not call.

github.overlay.yaml
name: github                  # tools are github.<tool>; permissions github.read / github.write
server:
  name: github
  transport: streamable_http
  url: https://api.githubcopilot.com/mcp/
  auth_token_ref: env://GITHUB_MCP_TOKEN
tools:
  get_issue:
    effect: read
    input_schema: {...}         # pinned by the snapshot
  create_issue:
    effect: write
    approval: true              # ask a person before every call
    timeout_seconds: 30
    input_schema: {...}
Rule fieldDefaultMeaning
effectrequiredread, write, external or destructive
approvaltrue for destructive, otherwise the agent's approval policyask before every call
timeout_seconds30per call
permissions<name>.read or <name>.writeexplicit permission list
max_result_bytes262144result size limit
descriptionthe server'swhat the model reads
input_schemarequiredthe reviewed schema

A server can change a tool at any time. python -m algen_agent_tools.mcp check github.overlay.yaml connects to the server and reports declared tools that are gone or whose schema changed, and tools the server offers that the overlay doesn't declare. It exits 1 on drift (with --strict, also on new tools) and prints JSON with --json, so you can run it on a schedule.

Load it from your project's plugin with overlay_plugin(Path(...).with_name("github.overlay.yaml"), name="acme.github", version="1.0.0", publisher="acme"), then name that plugin in domain_plugins. Do not also list the server under the Runtime's mcp_servers; the overlay owns the connection.

No idempotency key reaches an MCP server. Write tools are single-attempt and the Runtime's ledger stops a replay from calling twice, but a timeout leaves the outcome unknown. If an action must be exactly-once, that is the case for a native connector.

Govern

What every tool guarantees#

Every toolkit in the package passes the same contract harness, algen_agent_tools.testing.assert_toolkit_contract, on top of the Runtime's own tool contract:

  • Narrow and typed. No field, at any depth, takes free-form SQL, URLs, shell commands or vendor request bodies.
  • Honest about what was sent. A call refused before anything was sent, or definitely rejected by the provider, raises an error that is also a ToolNotAttemptedError, so the Runtime records it failed and it can be retried. A timeout or 5xx after a write was sent stays indeterminate for reconciliation.
  • Named and scoped. Every tool is <toolkit>.<action>, and every permission sits under the toolkit's prefix.
  • Governed by a preset. Reads are read(); anything that writes, sends or deletes is write, external or destructive, keyed and single-attempt. Destructive actions require approval by default.
  • Bounded. Timeouts, row counts, page sizes and result bytes all have limits.
  • Content-minimal errors. Errors name the failure type and the field, never the payload or a secret.
  • Outbound hosts are configuration. No tool argument chooses where a request goes.
Govern

Email and guardrails#

Email tools run under the Runtime's default guardrails; no guardrail setting is needed to send:

agent.yaml
agents:
  - name: notifier
    domain_plugins: [algen_agent_tools.smtp]
    enabled_tools: [smtp.send_email]
    tool_permissions: [smtp.write]
  • Recipients pass. Every address field (to, cc, bcc, reply_to) declares format: email, and the pii guardrail leaves a single address in such a field alone.
  • Everything else is screened. pii still checks display names, the subject and the bodies. If it redacts anything, the Runtime refuses the call with Payload transformed by policy.
  • Read addresses pass too. From 0.2.0a7 (Runtime 0.1.0a19 or later), Gmail, Outlook and IMAP results carry sender_address, recipient_addresses and cc_addresses, declared format: email, so the agent can reply to the sender it read. pii still redacts addresses in the display strings and bodies of results.

An agent that must quote addresses or phone numbers in a message leaves pii out of its tool boundaries. It still screens messages to and from the model:

agent.yaml
    guardrail_policy:
      tool_policies: [secrets, prompt_injection, output_validation]

Keep the agent's approval policy on: by default it asks a person before every external call. Use allowed_domains to bound who the agent can write to.

Govern

Secrets and environment#

Every setting is an env://NAME reference, resolved by the Runtime's secret resolver when the client first opens. Nothing is read at import or while a plugin registers, and no secret is logged, returned or put in an error. describe lists each plugin's variables so Studio can ask for them before a run.

PluginEnvironment variables
algen_agent_tools.postgresALGEN_TOOLS_POSTGRES_DSN
algen_agent_tools.mysqlALGEN_TOOLS_MYSQL_DSN
algen_agent_tools.sqliteALGEN_TOOLS_SQLITE_PATH
algen_agent_tools.smtpALGEN_TOOLS_SMTP_HOST, ALGEN_TOOLS_SMTP_PORT, ALGEN_TOOLS_SMTP_SECURITY, ALGEN_TOOLS_SMTP_USERNAME, ALGEN_TOOLS_SMTP_PASSWORD, ALGEN_TOOLS_SMTP_SENDER, ALGEN_TOOLS_SMTP_ALLOWED_DOMAINS
algen_agent_tools.gmailALGEN_TOOLS_GMAIL_CLIENT_ID, ALGEN_TOOLS_GMAIL_CLIENT_SECRET, ALGEN_TOOLS_GMAIL_REFRESH_TOKEN, ALGEN_TOOLS_GMAIL_SENDER, ALGEN_TOOLS_GMAIL_ALLOWED_DOMAINS
algen_agent_tools.outlookALGEN_TOOLS_OUTLOOK_TENANT_ID, ALGEN_TOOLS_OUTLOOK_CLIENT_ID, ALGEN_TOOLS_OUTLOOK_CLIENT_SECRET, ALGEN_TOOLS_OUTLOOK_MAILBOX, ALGEN_TOOLS_OUTLOOK_REFRESH_TOKEN, ALGEN_TOOLS_OUTLOOK_ALLOWED_DOMAINS
algen_agent_tools.imapALGEN_TOOLS_IMAP_HOST, ALGEN_TOOLS_IMAP_USERNAME, ALGEN_TOOLS_IMAP_PASSWORD, ALGEN_TOOLS_IMAP_PORT, ALGEN_TOOLS_IMAP_FOLDERS, ALGEN_TOOLS_IMAP_AUTHSERV_ID
algen_agent_tools.telegramALGEN_TOOLS_TELEGRAM_BOT_TOKEN, ALGEN_TOOLS_TELEGRAM_ALLOWED_CHATS
algen_agent_tools.discordALGEN_TOOLS_DISCORD_BOT_TOKEN, ALGEN_TOOLS_DISCORD_GUILD_ID, ALGEN_TOOLS_DISCORD_ALLOWED_CHANNELS
algen_agent_tools.githubALGEN_TOOLS_GITHUB_TOKEN
algen_agent_tools.samplenone

To use other variable names, or two databases at once, build the toolkit yourself with your own env:// references; see Configure in code.

Build

Configure in code#

Every connector has a toolkit factory that takes each setting as an argument. Build it in your own plugin to rename it, point it at other variables, bound recipients, or pass a fake in tests:

plugin.py
from algen_agent_runtime.plugins.sdk import PluginManifest, plugin
from algen_agent_tools.smtp import smtp_toolkit

notices = smtp_toolkit(
    "notices",                              # tool notices.send_email, permission notices.write
    host="env://NOTICES_SMTP_HOST",
    sender="env://NOTICES_FROM",
    allowed_domains=("example.com",),         # refuse other recipient domains
    artifact_loader=load_artifact,            # adds an attachments field
)


@plugin(PluginManifest(name="acme.tools", version="1.0.0", publisher="acme"))
class AcmeTools:
    def register(self, context):
        notices.register(context)
FactoryModule
PostgresToolkit(name, dsn=...), postgres_toolkit(...)algen_agent_tools.postgres
MysqlToolkit(name, dsn=...), mysql_toolkit(...)algen_agent_tools.mysql
SqliteToolkit(name, path=..., read_only=True), sqlite_toolkit(...)algen_agent_tools.sqlite
smtp_toolkit(name, host=..., sender=..., ...)algen_agent_tools.smtp
gmail_toolkit(name, client_id=..., client_secret=..., refresh_token=..., sender=...)algen_agent_tools.gmail
outlook_toolkit(name, mailbox=..., ...)algen_agent_tools.outlook
imap_toolkit(name, host=..., username=..., password=..., folders=...)algen_agent_tools.imap
telegram_toolkit(name, bot_token=..., ...), discord_toolkit(name, bot_token=..., ...)algen_agent_tools.telegram, algen_agent_tools.discord
github_toolkit(session_factory=None), github_overlay()algen_agent_tools.github
documents_toolkit(artifact_loader=..., quarantine="withhold")algen_agent_tools.documents
webhook_toolkit([WebhookTarget(...)])algen_agent_tools.webhook
sample_toolkit(name=...)algen_agent_tools.sample
McpOverlay.from_file(...), overlay_plugin(...)algen_agent_tools.mcp

Attachments: the artifact loader is async def load_artifact(reference, context) -> bytes. The model chooses the artifact id, so the loader must check that it belongs to context.tenant_id (and, if you scope artifacts to runs, context.run_id) before returning bytes.

Saving received files: Gmail, Outlook, IMAP, Telegram and Discord offer <name>.get_attachment only when the toolkit is built with an artifact_sink, the deployment's artifact store. Each file is checked against the attachment_policy (size limit, allowed types, and leading bytes that match the declared type), and the model gets back only the artifact reference, never the bytes.

Trust your own plugin's package under security.trusted_plugin_prefixes, as you do for algen_agent_tools.

Build

Testing#

Every connector ships an in-memory fake, so agent tests never touch the network:

FakeUse
algen_agent_tools.postgres.FakePostgresAnswers by SQL fragment, records every call and whether it ran read-only, refuses writes inside read-only transactions. PostgresToolkit("billing", client=fake.factory)
algen_agent_tools.mysql.FakeMysqlAnswers by SQL fragment and records whether each call ran read-only, as FakePostgres does.
algen_agent_tools.mail.FakeMailboxRecords sent messages, serves inbox pages and get_message, and from files= serves get_attachment. smtp_toolkit(client=FakeMailbox().factory())
algen_agent_tools.imap.FakeImapServerHolds folders of real MIME messages and answers as imaplib does; server.commands proves a test wrote nothing. imap_toolkit(imap_factory=server.factory())
algen_agent_tools.messaging.FakeMessagingA chat platform for the shared messaging tools.
algen_agent_tools.mcp.fake.FakeMcpServerAdvertises tools and answers calls. overlay.toolkit(session_factory=fake.session_factory)

Check your own toolkits with the same harness the package uses:

test_billing.py
from algen_agent_tools.testing import assert_toolkit_contract, plugin_tools

def test_billing_tools_meet_the_contract():
    assert_toolkit_contract(billing)

def test_the_plugin_describes_its_tools():
    assert set(plugin_tools("acme.plugins:AcmeTools")) == {"billing.list_open_invoices"}

Live tests in this package are marked live and run only with ALGEN_TOOLS_LIVE=1. Its end-to-end tests also run agents through the Runtime's real executor, for example to check that a context_params query's cached results stay with each caller.

Build

Write a connector#

Open a "Connector proposal" issue first; a connector needs an accepted proposal. src/algen_agent_tools/sample is the smallest complete connector to copy.

layout
src/algen_agent_tools/<connector>/
  __init__.py   # public API: the toolkit factory, its models and its fake
  toolkit.py    # <connector>_toolkit(...) -> Toolkit
  client.py     # the vendor client; optional imports happen here, lazily
  fake.py       # an in-memory stand-in used by tests
  plugin.py     # <Connector>Plugin = toolkit_plugin("<connector>", ...)
  • The factory takes every setting as an argument, with env:// defaults named ALGEN_TOOLS_<CONNECTOR>_<SETTING>, and a client override for the fake.
  • Tools are defined inside the factory, so each call returns an independent toolkit.
  • Nothing connects in the factory or in register. The client resolves its secrets and imports its optional package when it opens, and raises an error naming the extra if it is missing.
  • The docstring is what the model reads: say what the action does and when to use it.
  • Add an extra and an entry point in pyproject.toml, a docs page, unit tests against the fake, and opt-in live tests.
Build

Connector policy#

The package is curated. A new connector needs all of:

  1. Demand: at least two projects need it.
  2. No domain logic: it speaks to a vendor or protocol, not to one product. Hiring, payroll, text-to-SQL and similar logic belong in a domain pack.
  3. An owner: a named maintainer who reviews its changes and answers its security reports.
  4. A security review recorded in the pull request that adds it.
  5. MCP first: if a well-maintained MCP server covers the need, the package ships an overlay instead. A native connector is justified only when an overlay cannot give a guarantee the agent needs, such as an idempotency key or a typed action the server lacks.

Deprecations warn on use for at least one further minor release before removal, always with the replacement recorded in the changelog.

Reference

Tool reference#

Every tool the built-in plugins register, as algen-agent-runtime describe reports it:

ToolPluginSide effectPermissionTimeout
postgres.list_tablesalgen_agent_tools.postgresreadpostgres.read30 s
postgres.describe_tablealgen_agent_tools.postgresreadpostgres.read30 s
mysql.list_tablesalgen_agent_tools.mysqlreadmysql.read30 s
mysql.describe_tablealgen_agent_tools.mysqlreadmysql.read30 s
sqlite.list_tablesalgen_agent_tools.sqlitereadsqlite.read30 s
sqlite.describe_tablealgen_agent_tools.sqlitereadsqlite.read30 s
smtp.send_emailalgen_agent_tools.smtpexternalsmtp.write60 s
gmail.list_messagesalgen_agent_tools.gmailreadgmail.read120 s
gmail.get_messagealgen_agent_tools.gmailreadgmail.read60 s
gmail.get_threadalgen_agent_tools.gmailreadgmail.read120 s
gmail.send_emailalgen_agent_tools.gmailexternalgmail.write60 s
outlook.list_messagesalgen_agent_tools.outlookreadoutlook.read120 s
outlook.get_messagealgen_agent_tools.outlookreadoutlook.read60 s
outlook.get_threadalgen_agent_tools.outlookreadoutlook.read120 s
outlook.send_emailalgen_agent_tools.outlookexternaloutlook.write60 s
imap.list_messagesalgen_agent_tools.imapreadimap.read120 s
imap.list_foldersalgen_agent_tools.imapreadimap.read60 s
imap.get_messagealgen_agent_tools.imapreadimap.read60 s
imap.get_threadalgen_agent_tools.imapreadimap.read120 s
telegram.list_messagesalgen_agent_tools.telegramreadtelegram.read60 s
telegram.send_messagealgen_agent_tools.telegramexternaltelegram.write60 s
discord.list_conversationsalgen_agent_tools.discordreaddiscord.read60 s
discord.list_messagesalgen_agent_tools.discordreaddiscord.read60 s
discord.send_messagealgen_agent_tools.discordexternaldiscord.write60 s
github.* (37 tools)algen_agent_tools.github22 read, 13 write, 2 destructive; see GitHubgithub.read, github.write30 s
sample.echoalgen_agent_tools.samplereadsample.read5 s

Also registered, but only when built in code: <name>.get_attachment (read, 120 s) for Gmail, Outlook, IMAP, Telegram and Discord with an artifact_sink; <name>.read_attachment (read, 120 s) for Gmail, Outlook and IMAP with documents=DocumentReader(); documents.extract_text (read, 120 s); and one webhook.<action> (external, webhook.write) per webhook target. Typed SQL queries and your own MCP overlay tools are named by your toolkit or overlay (billing.list_open_invoices) and appear in describe the same way. The full contract of every tool is in the package's connectors.json, which each GitHub release also attaches.

Reference

Runtime compatibility#

algen-agent-toolsSupported algen-agent-runtime
0.2.0a10>=0.1.0a23,<0.2 (tested on 0.1.0a23)
0.2.0a9>=0.1.0a23,<0.2 (tested on 0.1.0a23)
0.2.0a8>=0.1.0a23,<0.2 (tested on 0.1.0a23)
0.2.0a7>=0.1.0a20,<0.2 (tested on 0.1.0a20 and 0.1.0a21)
0.2.0a6>=0.1.0a18,<0.2
0.2.0a1 to 0.2.0a5>=0.1.0a12,<0.2 (tested on 0.1.0a12 and 0.1.0a13)
0.1.0a2>=0.1.0a12,<0.2
0.1.0a1>=0.1.0a11,<0.2

The package is built on the Runtime's tool authoring API (@tool, presets, Toolkit), declared plugins and describe, all introduced in Runtime 0.1.0a11. From 0.1.0a2 it needs Runtime 0.1.0a12, whose pii guardrail leaves fields declared format: email alone. From 0.2.0a6 it needs Runtime 0.1.0a18: 0.1.0a14's document inspection reads XLSX, PPTX, HTML, CSV and TSV, and 0.1.0a18's cache_context keys cached SQL results on the values context_params binds. From 0.2.0a7 it needs Runtime 0.1.0a20, which records refusals as not attempted, keeps declared format: email result fields out of pii redaction and gives tools the run's attachments. From 0.2.0a8 it needs Runtime 0.1.0a23: 0.1.0a22 and 0.1.0a23 fix two ways an approval-required workflow tool step could run without its approval, and run synchronous tools in a worker thread within their timeout. CI runs the full suite against the lowest supported Runtime and the newest release inside the range; a Runtime release that breaks a connector lowers the upper bound until it is fixed. Every plugin targets plugin core API version 1.

Reference

Migrating from the Runtime's connectors#

Runtime 0.1.0a11 and earlier shipped a few connectors in its core. They moved here; the Runtime deprecates them and removes them later.

Runtime (deprecated)algen-agent-tools
communications.EmailMessage and the other message modelsalgen_agent_tools.mail (same names)
communications.SmtpEmailSender, SmtpSettingsalgen_agent_tools.smtp (same names)
communications.email_tool(sender)smtp_toolkit(...) or gmail_toolkit(...): tool <name>.send_email
communications.GmailMailboxClient and its modelsalgen_agent_tools.gmail (same names)
communications.gmail_inbox_tool(mailbox)gmail_toolkit(...): tool <name>.list_messages
tools.adapters.database_tool(name, handler)PostgresToolkit.query(...): one typed tool per query

The email tool changes on purpose: it is named smtp.send_email or gmail.send_email with permission <name>.write, the sender is fixed by the deployment instead of chosen by the model, custom headers are not accepted, and the timeout is 60 s. Update enabled_tools, tool_permissions and any policy that names the old tool communications.email.send. database_tool let the model send any statement; replace each statement the agent needs with a declared query.

Reference

Troubleshooting#

"Payload transformed by policy" when sending email

The pii guardrail redacted part of the message, usually an address or phone number in the subject or body, and the Runtime does not run a tool on redacted arguments. Recipients are not redacted with algen-agent-tools 0.1.0a2 and Runtime 0.1.0a12 or later, nor are the structured address fields of mail results with 0.2.0a7. Remove the detail from the message, or leave pii out of guardrail_policy.tool_policies; see Email and guardrails.

The plugin is not trusted, or is not found

Add algen_agent_tools (and your own plugin's package) to security.trusted_plugin_prefixes, and check the package is installed in the same environment as the Runtime. algen-agent-runtime describe lists the plugins it can see.

"install algen-agent-tools[...]" when a tool first runs

The connector's optional dependency is missing. Install the extra the message names, for example pip install "algen-agent-tools[postgres]".

An MCP tool is refused after a server update

The server changed the tool's input schema, or removed it. Run snapshot again, review the difference, and update the overlay's input_schema. Use on_drift: allow only when you accept unreviewed changes.

check reports an unknown tool

The name in enabled_tools does not match a registered tool. The message suggests the closest name; tool names are <toolkit>.<action>, so a renamed toolkit renames its tools.

A database read fails with a row limit error

The query returned more rows than its limit (500 by default) and raised TooManyRows. Narrow the query, or raise max_rows when declaring it; reads never truncate silently. This applies to PostgreSQL, MySQL and SQLite alike.

Every cached tool call fails with "Internal error (ValueError)"

The tool-result cache is scoped to session. On Runtime 0.1.0a18 the tool cache gets no session, so every cacheable call fails; Runtime 0.1.0a19 fixed it, and 0.2.0a7 requires 0.1.0a20 or later. On 0.1.0a18 use user or run scope instead. A context_params query is safe at any scope: its cache key includes the values it binds.

Telegram reads fail with error 409

The bot has a webhook, or another program is polling it. getUpdates works only when this connector is the bot's only reader; delete the webhook or use a separate bot.

Discord messages come back with empty text

Turn on the bot's Message Content privileged intent in the Discord developer portal. Without it Discord returns text only for messages that mention the bot.

sender_auth is always unknown on IMAP

On servers other than Gmail, set ALGEN_TOOLS_IMAP_AUTHSERV_ID to the name your receiving server writes at the start of its Authentication-Results header (for example mx.zoho.com). A header the connector can't attribute to your server proves nothing, so it is ignored.

Reference

Source and links#

No sections match your search. Try a different term.