Status: M12A implemented, including identity manifests, delegated provisioning, durable channel grants, grant-enforced native participation, human-facing disclosure, bounded invocation/guardrails, the reference MCP adapter, and publisher, deterministic command, roster utility, durable worker, and model-backed examples
Automated participants are durable, visibly non-human accounts with narrowly scoped channel access. The IRC server owns their identity, admission, event provenance, and delivery limits. Bot logic, model runtimes, prompts, memory, external tools, and provider credentials remain outside the IRC daemon.
This substrate covers deterministic bots, feed publishers, externally hosted content services, and model-backed agents. It does not make the server an AI platform.
The domain model distinguishes human accounts from automated accounts. Automation has a visible kind:
- a
botis deterministic automation such as a news feed, bridge, or external content catalogue; - an
agentmay send channel content to a model or other reasoning service.
Both kinds use stable account IDs and independently revocable, labeled app
keys. An account cannot mark itself human after it has been provisioned
as automation. Ordinary invite redemption creates a human account only;
creating an automated account requires infrastructure authority or the narrow
account.automation.provision capability.
Every bot or agent is created transactionally with a bounded, operator-approved manifest. It records the immutable account kind, a purpose, a data-handling statement, and one processing boundary:
localruns on the IRC server and names no separate processor;networknames a processor elsewhere inside the operator's network; orexternalnames the outside service receiving granted content.
The manifest grants no channel access. It contains no provider credential, prompt, tool authority, model memory, or proof that the processor honors its statement. It is identity state, so content reset preserves it alongside the account and app keys. Startup and integrity checks reject a bot or agent without a manifest, or a human account with one.
Offline infrastructure provisioning is available while the daemon is stopped:
telex-ircd account automation create bot ReleaseBot release-publisher local \
"Publishes release announcements" \
"Receives only granted events and stores no separate history"
telex-ircd account automation create agent Helper production external \
"Example Model Service" \
"Answers explicit channel questions" \
"Granted text is sent to the named service; provider retention may apply"
telex-ircd account automation show Helper
Provisioning prints the initial account/key-id authentication ID and random
ak1... key once. Only its SHA-256 digest is stored. Infrastructure-owned
deployments remain manageable while the daemon is stopped:
telex-ircd account automation key add Helper staging
telex-ircd account automation key list Helper
telex-ircd account automation key revoke Helper <key-id>
An authenticated human steward holding account.automation.provision can use
POST /v1/automated-accounts. The capability is checked before app-key
generation and again inside the account-and-manifest transaction. There
is deliberately no global automation directory endpoint.
Automation provisioned through that live endpoint is durably owned by the
human account that created it. The same account can use /automation/ to list
only its automation, add independently revocable app keys, and revoke one key
without disconnecting the account's other deployments. New key material is
shown once; the browser retains the human password and shown key only in tab
memory. Revocation scrubs the stored key digest, invalidates pending native
tickets for that key, and closes its active native session. Infrastructure-
provisioned and legacy automation for which no durable creator can be recovered
remains ownerless and CLI-managed rather than being silently assigned.
The same console lists only channels where the authenticated human is currently an owner or operator. Selecting one returns a revision-consistent automation snapshot and drives the existing grant, chatter, guardrail, and pause endpoints. An exact account name may still be granted even when its automation belongs to another human; channel authority and automation ownership are separate.
Newly provisioned automation app keys remain dormant until at least one channel grant exists. They may then obtain a native session, but every JOIN, replay, live event, acknowledgement, publication, reply, and revocation still checks the exact account/channel authority. Direct IRC login for automated app keys remains disabled because a conventional joined IRC bot receives a whole channel stream and cannot preserve invocation-only or publisher-only grants without another adapter policy.
Public-channel visibility does not imply automated access. Unlike a human account, an automated account may join, read, or publish only in channels named by its grants. It receives no network-wide discovery, invitation, moderation, direct-message, or private-history authority by virtue of being a bot.
The stored authority consists of individual grants rather than one broad
bot_allowed bit:
- receive messages that explicitly invoke the account;
- read new human-authored events;
- read new automation-authored events;
- read retained history, with an optional server-enforced maximum window;
- send a reply tied to a delivered invocation or event;
- publish an unsolicited event;
- inspect the current channel roster;
- consume individually authorized external packages; and
- perform explicitly authorized bulk package retrieval.
This authority is now durable. A channel owner or operator can set or remove a
grant with an expected policy revision, and can independently enable or disable
automation-to-automation chatter. Each applied mutation advances the same
channel-policy revision used by ordinary moderation and appends one immutable,
bounded policy event. Stale writers cannot overwrite the winner. The server
accepts at most 32 automated accounts per channel, and a history grant must name
a window from one minute through 400 days. Bulk content authority implies
ordinary content consumption, while reply authority requires at least one
server-delivered input class. Every grant explicitly states an invocation
context allowance from zero through ten events. A nonzero allowance requires
invocation.receive; grants without that permission must state zero.
The policy state and its events are removed by content reset while the account
and disclosure manifest survive. Removing the last grant immediately ejects
every connected session from that room and prevents issuance of a new native
ticket until another grant exists. Removing a grant, or narrowing it to remove
live.human.read, also removes that account's optional command catalogue from
the channel.
Reading automation-authored events requires two independent decisions: the channel policy must permit automation chatter, and the receiving account must hold the relevant live or retained-history authority. The switch applies to both live delivery and replay, so reconnecting cannot bypass it. An automated account never receives its own events as input. Bot traffic is not prohibited network-wide: deliberately automated rooms can let bots and agents converse for as long as their operating budgets permit.
Named presets may simplify administration, but presets are only user-interface shorthand for explicit grants:
| Preset | Typical grants | Example |
|---|---|---|
| Publisher | Unsolicited publish only | News or release feed |
| Query bot | Invocation, reply, optional bounded history | External-topic search or channel-history answers |
| Participant | Human live events and replies | General channel agent |
| Automation-room participant | Human and automation live events and replies | Bot-to-bot discussion space |
A query invocation is a human-created ordinary channel message addressed to
one admitted account. A native human can use automation.invoke; an
authenticated ordinary IRC user can use the strict leading form
@Account: request. Both paths commit the same visible message and private
target relation when the name resolves to an admitted account with
invocation.receive. Similar text later in a sentence, malformed addressing,
NOTICE, and TAGMSG remain ordinary chat behavior. A strict leading mention
of an unknown name, a human, or automation without invocation authority also
remains ordinary chat; this preserves ambient mentions for participant-style
agents. A configured invocation target that is paused, disabled, banned, or in
cooldown returns one opaque failure and creates no event. The explicit native
automation.invoke operation continues to fail opaquely for every unavailable
target rather than falling back to chat.
invocation.receive releases that visible event plus the zero-to-ten preceding
retained events configured on that exact channel/account grant. Context
excludes the target's own events and includes automation-authored events only
when the room's chatter switch is enabled. An offline invocation-only account
can resume addressed events from its app-key checkpoint. Access to general
history and future messages remains separate. The initial release keeps agent
DMs disabled; queries happen visibly inside a channel.
People must be able to tell that automation is present and whether their text can cross the network's processing boundary.
Each automated account has operator-approved descriptive metadata including its kind, purpose, and a data-handling declaration. An agent declaration says whether processing is local to the server, operated elsewhere inside the operator's infrastructure, or sent to a named external service. A declaration is a statement by the automation operator, not proof of an external provider's retention behavior.
Adding, removing, or materially expanding an automated account's grants is a
durable channel event. Native JOIN results contain the complete bounded roster,
processing declarations, exact permissions, history windows, chatter state,
guardrail policy, active cooldowns, and policy revision; later changes arrive
as full automation_policy replacements. The reference client displays that
state before replaying cached messages and again whenever it changes.
A human IRC client receives one concise server NOTICE on JOIN and on every live
policy change whenever the room has admitted automation. It summarizes the
revisioned durable policy rather than merely listing bots that happen to be
online: an offline agent with history.read can still receive later text and
must therefore remain visible. An automation connection joining or reconnecting
does not repeat that policy into chat. Empty ordinary rooms retain their
traditional JOIN reply sequence without a meaningless automation notice.
For clients with message-tags, the NOTICE carries
telex/automation-policy, telex/automation-count,
telex/automation-state, and telex/automation-chatter; a live mutation also
carries the valueless telex/automation-change tag. The bundled web client
recognizes a valid server-authored snapshot, keeps the newest revision as
replaceable channel state, and does not put JOIN-time snapshots in the message
timeline. Traditional clients ignore the tags and display the single readable
NOTICE. Exact manifests, permissions, context limits, processing declarations,
guardrail budgets, and cooldowns remain available through the structured
native and authenticated management surfaces.
An automation that accepts ordinary leading-! messages may publish an
optional durable command catalogue for each granted channel. Capable human
clients negotiate telex/bot-commands and receive one revisioned atomic
snapshot on JOIN and after replacement; traditional clients receive nothing
extra. Each declaration contains a bounded lowercase command name, optional
usage, and terse description. It is presentation metadata only: the server
does not parse, dispatch, authorize, or execute the declared command.
Publishing requires the same channel membership and live.human.read grant
needed for the bot to receive ordinary human messages. This deliberately
keeps discovery aligned with actual input visibility. A catalogue can contain
32 commands, the channel aggregate is 128, identical retries are silent, and
an empty replacement clears it. Duplicate names from different bots remain
separate declarations rather than silently establishing server-side routing.
The server implements
IRCv3 Bot Mode, advertises
BOT=B, reports active automation through WHO and WHOIS, and adds the bare
bot message tag for clients that negotiated message-tags. The server,
rather than the automation connection, projects immutable bot or agent account
classification as read-only user mode +B; client attempts to set or clear it
are rejected. Native clients render bot and agent separately.
The canonical integration is the authenticated native gateway:
- an automated deployment resumes its permitted channel streams from durable delivery checkpoints;
- the server emits only events allowed by that account's channel grants;
- the external runtime processes an event and submits a message with an idempotency key and, where applicable, a cause event;
- the server validates the current grant and commits the event through the ordinary event path.
Provider API keys, system prompts, tool credentials, and model memory never enter the IRC database. Revoking the automation app key or its channel grant stops future delivery without changing human credentials.
Retained replay resolves the current grant, history window, chatter switch, guardrail state, and event-author filter inside one serialized store operation. Live delivery rechecks the grant and guardrail state for each event. A causal reply may name only one of the last 1,024 events actually released in that native session; reconnecting does not turn a guessed retained event ID into proof of delivery. An exact idempotent retry of an already committed operation remains recoverable after reconnect as long as retention still preserves its provenance row.
The dependency-free reference MCP adapter exposes permitted input as one bounded NDJSON resource per explicitly configured channel, provides pageable retained history and a minimal current-nickname roster when independently granted, and exposes publication and causal reply as separate tools. It commits the complete server-selected invocation context before acknowledgement, marks only released top-level events as replyable, reapplies current grant classes to cached content, emits standard resource/tool change notifications, and resumes automatically after cooldown. It does not expose human invocation as a bot-to-bot shortcut; selected bot rooms use ordinary visible messages under chatter policy and independent grants.
MCP is an adapter, not the authorization boundary: server-side grants remain authoritative regardless of an agent's prompts or skills. The protocol's host/client/server separation fits this design without coupling the IRC daemon to one model vendor. A skill can teach a runtime how to invoke the adapter, but cannot grant access. The adapter does not schedule turns or call a model; the MCP host or a separate automation runner owns that behavior.
The dependency-free CommandBot example
shows the smaller deterministic shape. It self-checks an exact
invocation.receive plus message.reply grant with zero context, attenuates
its native ticket to one channel and four operations, and journals the chosen
reply before publication so an uncertain retry remains idempotent. It is an
interface scaffold, not a news service or model integration.
The dependency-free
CatalogBot example shows the
complete leading-! shape. It self-checks exactly live.human.read plus
message.reply, retains no channel text, atomically publishes the three
commands it actually handles, ignores unknown command names, and derives each
causal reply operation ID from its source event. It therefore demonstrates
client discovery and ordinary-message delivery without silently widening the
invocation-only CommandBot.
The one-shot
command-catalogue publisher
shows the separate leading-! shape. It reads one bounded JSON declaration,
attenuates its ticket to channel.join and automation.commands.set, joins
the channel, and atomically replaces the authenticated bot's catalogue with
compare-and-set retry handling. It intentionally implements no command
behavior; the bot runtime remains an independent ordinary-message consumer.
The ScheduledPublisher example
shows a timer-owned process that receives no channel input, requests only
message.publish, and exits after one publication. Its explicit UTC occurrence
becomes a deterministic operation ID, so a scheduler can retry the same run
without maintaining another delivery database while the committed event and
its provenance remain retained.
The RosterBot example composes
roster.read with the same zero-context invocation/reply contract. Each direct
who [after NICK] request reads at most one eight-name page, journals the
selected response before publication, and acknowledges the invocation only
after that causal reply commits. It demonstrates current-room utility without
granting the general channel feed.
The DurableWorker example
shows the asynchronous boundary. It persists a bounded local job before
causally accepting and acknowledging the invocation, recovers interrupted work
locally, and later uses message.publish with a stored operation ID for the
completion. Telex makes input delivery and message retries durable; the worker
still owns job state and must make real external work idempotent itself.
The Chatbot example shows the participant-style shape. Its account grant, channel chatter policy, and current guardrails determine which raw events and MCP tools exist. A separate runner chooses whether to invoke a model for every event, only for highlighted mentions, or for timer/count batches with mentions flushing early. The runner, not Telex, constructs the model request and owns its instructions, provider bearer token, scheduling, and checkpoint. This keeps the server's authority line at event and operation visibility rather than prompt construction.
Existing IRC libraries can be placed behind a native adapter, but automated app keys are not accepted directly through conventional IRC SASL. The native gateway is required for invocation-only delivery because a traditional joined IRC client normally receives the entire live channel stream. Human IRC clients still see native bots as ordinary visible channel members.
Every automation event remains an ordinary retained channel event, visibly authored by the automated account. Its immutable envelope may additionally carry:
- the event or invocation that caused it;
- an automation conversation identifier;
- the submitting operation ID used for idempotency; and
- the automation kind needed for presentation and delivery policy.
These fields never contain prompts, model credentials, or hidden reasoning.
Bot chatter is controlled rather than categorically disabled. Exact idempotent retries are resolved before accounting; every new automated append then checks and charges one durable fixed-window budget inside the same immediate SQLite transaction that would commit the event. The per-account counter is shared by all app keys for that account in that room, and the aggregate counter covers all automation in the room. Concurrent deployments therefore cannot spend the same remaining allowance. A rejected operation is neither retained nor broadcast.
The one-minute defaults are 60 messages and 32 KiB per account, and 240 messages and 128 KiB for the channel in aggregate. A trip starts a 30-second account or channel cooldown. Channel owners and operators can revision-safely tune the policy within hard daemon bounds:
| Setting | Minimum | Default | Maximum |
|---|---|---|---|
| Account messages per minute | 1 | 60 | 600 |
| Account bytes per minute | 512 | 32 KiB | 512 KiB |
| Channel messages per minute | account limit | 240 | 10,000 |
| Channel bytes per minute | account limit | 128 KiB | 8 MiB |
| Cooldown | 1 second | 30 seconds | 1 hour |
The server also suppresses simple loops without imposing a conversation turn limit. It lowercases and collapses whitespace for an operational SHA-256 signature: three equal automated messages may pass in one minute, while the fourth starts an account cooldown when one account repeated it or a channel cooldown when several accounts did. For causal replies it follows at most 16 ancestors; a proposed message matching two automated ancestors in that chain is the third occurrence and starts a channel cooldown. These are deliberately conservative tripwires, not a semantic claim that the server understands the conversation.
There is no mandatory turn-count ceiling in an automation-enabled room. A room created to watch agents converse may run continuously when its messages vary and remain inside its configured budgets. Runtime-side limits must separately bound model tokens, money, execution time, tool calls, and provider concurrency because the IRC daemon cannot measure those resources reliably.
An automatic trip is immediately visible to human IRC members as a bounded
server notice and to native clients as a full policy snapshot with its scope,
reason, and expiry. The trip itself is short-lived operational state, not a
permanent content event. A channel owner or operator can also set a durable,
revisioned moderator pause. Pause and cooldown block automated publication,
resume, live input, and invocation delivery while human JOIN, history, and chat
remain unchanged. A human invocation against an unavailable target retains the
same opaque invocation_unavailable result used for missing, ungranted, or
disabled automation.
Pause or cooldown suspends an automated live subscription. Once the state
clears, the adapter explicitly resumes from its durable app-key checkpoint
before becoming live again. Retained events from the interval remain eligible
under the current grant and history window; live_only events are not secretly
buffered. Guardrail-policy changes and pause transitions clear all transient
accounting, grant removal clears that account's usage row, and content reset
removes every channel-scoped row, so deliberate administrative changes do not
inherit a stale cooldown.
The authenticated control endpoints are:
POST /v1/channel-automation-guardrails
{"channel":"#agents","expected_revision":7,
"account_message_budget":60,"account_byte_budget":32768,
"channel_message_budget":240,"channel_byte_budget":131072,
"cooldown_seconds":30}
POST /v1/channel-automation-pause
{"channel":"#agents","expected_revision":8,"paused":true}
They use the same TLS, HTTP Basic, source throttling, owner/operator authority, and stale-revision behavior as the existing automation grant controls. Every applied policy or pause transition advances the shared channel-policy revision, stores one bounded metadata-only policy event, clears transient accounting, and pushes a complete native replacement plus one compact tagged IRC snapshot.
The owner-scoped management endpoints used by the bundled console are:
GET /v1/automation-management
GET /v1/automation-channels/<percent-encoded-channel>
POST /v1/automated-accounts/<account>/app-keys
DELETE /v1/automated-accounts/<account>/app-keys/<key-id>
The management index contains only automation owned by the authenticated human and channel names where that human currently holds owner/operator authority. It is not an account, automation, or channel directory. Each automation entry includes at most 32 key records, ordered so every possible active key precedes the most recent revoked entries; an explicit flag reports omitted older revoked records. Channel snapshots recheck owner/operator authority in the serialized store operation. Key mutations recheck durable ownership in the transaction and collapse unknown and foreign targets to the same unavailable response.
Automation does not respond to its own events by default. Agent runtimes must treat channel text as untrusted input; a channel read grant never implies access to administration commands, secrets, files, or external side-effecting tools.
The server can prove which events it released through the gateway and stop future release after revocation. It cannot prove that an external runtime or model provider forgot content it already received. The disclosure surface must say this plainly.
Reference runtimes keep state isolated per channel by default and do not build cross-channel participant profiles. They consume content-epoch changes and discard their server-managed channel cache on reset. This is cooperative local cleanup, not remote erasure: provider logs, deliberately exported data, and independent bot storage remain outside the server's control.
Loop accounting stores no message body: only a channel-scoped SHA-256 digest of lowercased, whitespace-collapsed content, occurrence metadata, and bounded message/byte counters. Rows expire after the one-minute window through either a subsequent automated publication or the ordinary startup/periodic retention pass. A future operator with both a guessed message and the database could test that guess against the digest; this is minimization, not encryption.
- hosting or selecting models inside the IRC daemon;
- server-managed prompt or vector-memory storage;
- hidden agents or undisclosed passive readers;
- agent direct messages;
- granting moderation or invitation authority to automation;
- verifying claims made by an external model provider;
- server-hosted or server-proxied file bytes.
The same service-account substrate supports the implemented
XDCC-like external content interface. Package policy
and separate content.consume and content.bulk grants prevent ordinary agent
access from implying permission to mirror a catalogue.
Live Stage publication is similarly separate: stage.publish permits an
automation to occupy a granted channel's transient Stage, while
message.publish alone only permits retained chat publication. Stage frames
never enter automation history, message budgets, or channel retention.
- whether direct IRC automation should support only full-feed grant presets or gain a separate negotiated extension for narrower delivery;
- whether a future opt-in direct thread with an agent should use ordinary DM controls or a separate invocation conversation.