Status: M11 vertical slice frozen; M12A automation and M12B external-content extensions implemented
For setup, interface selection, reference-client commands, and a minimal client example, start with the native gateway practical guide. This document is the exact wire and security contract.
The native gateway is an optional structured adapter inside the IRC daemon. It shares accounts, installations, live channel admission, event history, delivery checkpoints, content epochs, authorization, rate policy, and the serialized SQLite writer with the IRC adapters. It is not a second chat backend.
Traditional IRC and IRC-over-WebSocket remain complete compatibility paths. Native clients may use this API for exact resume and structured extensions; grant-scoped automation and its reference MCP adapter now use the same boundary, and provider-hosted catalogue/capability exchange uses five additional scoped operations without moving package bytes into chat storage.
The existing TLS control listener owns both endpoints:
POST /v1/native-sessionsauthenticates a human with bare account/password HTTP Basic or automation withaccount/key-idplus itsak1...app key, then returns one short-lived session ticket.GET /v1/syncupgrades to WebSocket only when the client offers theirc.native.v1subprotocol. Every application message is one UTF-8 JSON object.
Remote plaintext remains impossible on the control listener. Browser requests must pass the existing exact same-origin check. The API sets no cookie, enables no permissive CORS policy, and accepts no bearer secret in a URL.
The session response is:
{
"api_version": "1",
"ticket": "nt1.<base64url secret>",
"expires_in_seconds": 30,
"device_id": "019f...",
"installation_token": "dr1.019f....<secret>",
"access": {
"operations": [
"channel.join",
"sync.resume",
"history.read",
"roster.read",
"delivery.ack",
"message.send",
"automation.invoke",
"automation.commands.set",
"content.search",
"content.request",
"content.package.upsert",
"content.package.remove",
"content.transfer"
],
"channels": null
}
}A human client's first response includes installation_token; it sends that
value later in Telex-Installation. A bound response omits the field while
returning the same device_id. The token identifies a delivery cursor only and
cannot authenticate. Automated responses use the app-key ID as device_id and
never accept an installation header.
A ticket contains 256 random bits, is stored only as a keyed lookup digest in process memory, expires after 30 seconds, and is removed on its first exchange. At most 256 unexpired tickets exist. A process restart invalidates them. The ticket grants no durable authority and never replaces the account password or app key.
An empty request body preserves the unrestricted human-client behavior above. An issuer can instead ask the server to attenuate that one ticket:
{
"operations": ["channel.join", "sync.resume"],
"channels": ["#news", "#research"]
}Operations are an explicit subset of the thirteen v1 command names. channels is
either null for every channel the account itself may enter or a list of at
most 32 valid IRC channel names; an empty list grants no channel. The server
RFC1459-casefolds, deduplicates, and echoes the effective access in both the
HTTP response and ready. Every command and live delivery rechecks it, and a
denial returns scope_denied without expanding the account's ordinary channel
authority.
This is capability attenuation, not a new durable credential system. A trusted launcher can retain the account password or app key and hand a short-lived, one-use, read-only ticket to a less-trusted tool. A process that possesses that authentication secret can simply request another scope and therefore is not confined by this mechanism. Durable bot/agent manifests and channel grants further restrict every automated session and cannot be overridden by a ticket or app key.
The first WebSocket application frame must arrive within five seconds:
{
"type": "hello",
"api_version": "1",
"ticket": "nt1.<base64url secret>",
"nickname": "Alice"
}The server consumes the ticket, rechecks that the account is enabled and has the immutable kind captured at authentication, validates and atomically claims the IRC-compatible nickname, registers the account and installation/key as a live client, and derives its username from the canonical account name. Automated accounts additionally require their mandatory manifest and at least one current channel grant. A successful response contains the account's own typed identifiers, the current content epoch, and the active bounds:
{
"type": "ready",
"api_version": "1",
"session_id": "019...",
"account_id": "019...",
"device_id": "019...",
"content_epoch": "019...",
"access": {
"operations": ["channel.join", "sync.resume"],
"channels": ["#news", "#research"]
},
"limits": {
"channels": 32,
"history_batch": 100,
"roster_page": 100,
"incoming_frame_bytes": 16384,
"outgoing_frame_bytes": 131072,
"invocation_context_events": 10,
"catalogue_results": 25,
"bulk_catalogue_results": 50,
"content_capability_seconds": 300
}
}The session ID is ephemeral presentation metadata. Accounts, devices, and delivery checkpoints remain the only durable session and cursor identities. Closing the socket removes the live participant and emits ordinary PART/QUIT presentation to compatible clients.
Every command is a tagged JSON object. Unknown fields, unknown command names,
non-canonical identifiers, invalid strings, and frames outside the documented
bounds are rejected. request_id and operation_id values are canonical
UUIDv7 identifiers.
{
"type": "channel.join",
"request_id": "019...",
"channel": "#general",
"key": null
}Joining invokes the same public/invite-only membership, account ban, +k, +l,
channel-count, and live policy-revision checks as IRC JOIN. A native subscriber
is therefore a visible member, not a hidden history reader. The committed
result includes the stable conversation ID, display name, effective retention,
and current automation trust snapshot:
{
"type": "result",
"api_version": "1",
"request_id": "019...",
"operation": "channel.join",
"status": "joined",
"result": {
"kind": "channel",
"conversation_id": "019...",
"channel": "#general",
"retention": {"mode": "finite", "milliseconds": 2592000000},
"automation": [{
"account_name": "Helper",
"kind": "agent",
"purpose": "Answers explicit channel questions",
"processing_boundary": "external",
"processor": "Example Model Service",
"data_handling": "Granted text is sent to the named service",
"permissions": ["invocation.receive", "message.reply"],
"history_window_seconds": null,
"invocation_context_events": 10,
"policy_revision": 7
}],
"automation_chatter_enabled": false,
"automation_guardrails": {
"paused": false,
"window_seconds": 60,
"account_message_budget": 60,
"account_byte_budget": 32768,
"channel_message_budget": 240,
"channel_byte_budget": 131072,
"cooldown_seconds": 30,
"repeated_content_limit": 3,
"causal_signature_limit": 2,
"channel_cooldown": null,
"account_cooldowns": []
},
"automation_policy_revision": 7
}
}The automation list is the durable grant roster, not merely current presence;
an offline history reader remains disclosed. Automated accounts can enter only
an existing explicitly granted channel, cannot create a public room by naming
it, and do not use a human membership role or shared +k secret to bypass that
grant. Bans still win. The ticket must include channel.join and permit the
canonical channel name.
The guardrail object is also a complete replacement snapshot. Cooldowns name
an account or channel scope, a bounded reason, and an absolute Unix
millisecond expiry; account cooldowns additionally name the disclosed account.
The durable policy revision may remain unchanged when only this short-lived
operational state changes.
{
"type": "sync.resume",
"request_id": "019...",
"conversation_id": "019...",
"limit": 100
}The conversation must currently be joined by this session. The server reads
after the existing (account, device, conversation) delivery checkpoint and
returns an ascending event_batch. At most 100 retained events appear. If the
batch is not exhausted, the client durably processes and acknowledges it before
requesting the next page. Once exhausted, the subscription becomes live.
Resume and live delivery require sync.resume in the ticket.
Each retained event contains its event and conversation IDs, offset, receive time, message kind, retained text, author presentation, and optional native provenance. Offsets may have gaps because attribution-free retention tombstones remain valid cursor anchors but are not released as history. Events do not expose another participant's device, address, local session, or internal account ID.
The subscription becomes live only after an exhausted page. Commit-ordered
notifications are filtered against current live membership again immediately
before delivery. Events committed while a joined session has not yet completed
resume use a bounded in-memory handoff queue and are deduplicated by the durable
conversation offset. Queue or broadcast lag closes the stream with
stream_lagged; reconnect and checkpoint resume are the recovery path.
For a human account, resume retains the ordinary device semantics above. For a
bot or agent, one serialized store operation rechecks the current channel grant
and applies its history.read window before releasing retained events. Without
history authority, resume establishes only a live barrier when the grant has a
live or invocation input class; a publish-only account cannot use resume.
Future human events require live.human.read. Future bot/agent events require
live.automation.read plus the channel's default-off chatter switch. Retained
bot/agent events likewise require the chatter switch, and self-authored events
are never returned as automation input. An active moderator pause or applicable
cooldown rejects resume and suspends an existing automated live subscription.
The adapter explicitly resumes after the reported state clears. Its durable
checkpoint makes retained events recoverable under the then-current grant;
live_only content is not buffered by the server.
{
"type": "history.read",
"request_id": "019...",
"conversation_id": "019...",
"before_event_id": null,
"limit": 100
}This is a separate, read-only retained-history operation for an automated
account. The session must be joined, its ticket must allow history.read, and
the current channel grant must contain the permission with a finite server-side
window. A human native client uses the ordinary history and delivery APIs
instead.
The server returns the newest available page in ascending channel order:
{
"type": "history_page",
"api_version": "1",
"request_id": "019...",
"conversation_id": "019...",
"events": [],
"next_before_event_id": null,
"exhausted": true
}When more history exists, next_before_event_id is the oldest event in the
page. Supply it as the next request's exclusive before_event_id. The same
serialized read rechecks the current grant, history window, pause/cooldown,
automation-chatter switch, author kind, and self-event exclusion. Returned
events are ordinary authorized history without invocation context.
Unlike sync.resume, this operation neither reads nor advances the deployment
delivery checkpoint and never establishes a live subscription. Events in a
successful page do enter the session's bounded delivered-cause set, so an
account with message.reply can causally reply to one without first moving its
durable delivery cursor. This separation lets an MCP host page context on
demand without redefining “processed delivery.”
{
"type": "roster.read",
"request_id": "019...",
"conversation_id": "019...",
"after_nickname": null,
"limit": 100
}This is an on-demand presence snapshot for an automated account. The session
must be joined, its ticket and channel allowlist must permit roster.read, and
the current channel grant must still contain that permission. The server also
rechecks moderator pause and applicable account/channel cooldown state.
{
"type": "roster_page",
"api_version": "1",
"request_id": "019...",
"conversation_id": "019...",
"entries": [
{"nickname": "Alice", "prefix": "@", "bot": false},
{"nickname": "Helper", "prefix": "", "bot": true}
],
"next_after_nickname": null,
"exhausted": true
}Each active connection contributes one entry, including the requester. Two
devices using different nicknames therefore remain two entries even when they
belong to one account. prefix is the current NAMES role projection (@, +,
or empty), and bot is the server-asserted +B state for either a bot or
agent account. Entries deliberately omit account names and IDs, device/session
IDs, username/host/address/TLS data, offline membership, last-seen state, and
history.
Pages are strictly RFC1459-casefolded nickname order. When more entries exist,
next_after_nickname is the last nickname returned; send it as the next
exclusive after_nickname. Membership can change between calls, so a
multi-page traversal is weakly consistent. The operation stores nothing,
advances no cursor, and does not subscribe the automation to JOIN/PART changes.
{
"type": "delivery.ack",
"request_id": "019...",
"conversation_id": "019...",
"event_id": "019..."
}The event must belong to the joined conversation. The existing monotonic
delivery checkpoint advances transactionally or returns its unchanged current
position. Acknowledgement means that this device claims durable processing; it
does not change the account-wide human read marker. It separately requires
delivery.ack in the ticket. An automated session may acknowledge only an
event the server actually released in that session; knowing or guessing another
event ID is insufficient.
{
"type": "message.send",
"operation_id": "019...",
"conversation_id": "019...",
"text": "hello",
"cause_event_id": null
}The sender must currently be joined, have message.send in its ticket, and fit
the same IRC-visible line bound. An automated sender additionally needs the
current channel's message.publish permission when no cause is supplied, or
message.reply when a cause is supplied. That permission check and the append
share one SQLite transaction.
The optional cause must name a retained earlier event in the same conversation.
The operation ID, a SHA-256 digest of the canonical
(conversation, text, cause) tuple, and the cause are committed atomically with
the ordinary event. Retrying an identical retained operation returns the
original event without another IRC or native broadcast; reusing the ID with
different content is rejected. The request digest and originating account stay
private store metadata. Authorized event recipients see only the operation ID
and optional cause.
For a new automated operation, the current account/channel message and byte
budgets, repetition signature, causal-loop bound, pause, and cooldown are
checked and charged atomically with append. Exact retries are resolved first
and consume no second allowance. automation_paused is non-retryable until a
policy replacement changes state; automation_cooldown is retryable after the
expiry carried by the subsequent guardrail snapshot.
For automation, the cause must also be among the bounded set of events actually released in the current session. This prevents a guessed event ID or broader database visibility from manufacturing a reply edge. Exact retained-operation retries are resolved before that ephemeral proof check so a reconnect can learn the result of its own already committed write without committing it again.
Idempotency metadata expires when retention prunes its event and disappears on
content reset. A causal edge is also cleared as soon as its cause is pruned.
For a live_only channel this privacy rule makes the idempotency record expire
at the initial commit, so a later retry has no durable deduplication window.
The resulting event is ordinary channel history and is projected to IRC and native recipients from the same commit. The gateway does not add native-only messages.
{
"type": "automation.invoke",
"operation_id": "019...",
"conversation_id": "019...",
"account_name": "Helper",
"text": "what did we decide?"
}Only a joined human native account may invoke an automated participant, and
the ticket must independently allow automation.invoke for that channel. The
target name is resolved without disclosing whether an account, manifest, or
grant is missing. The target must be an active bot or agent with the current
channel's invocation.receive permission. Automated callers use the ordinary
live/chatter and causal-reply paths instead; they cannot recursively invoke
through this operation.
Pause and cooldown are folded into the same opaque target decision. A human
therefore receives invocation_unavailable, not evidence that a named account
exists or which guardrail currently applies to it.
The committed event is an ordinary visible channel message rendered as
@Helper: what did we decide?. IRC clients, native humans, and other readers
with ordinary channel authority see that text, but not the private target
relation. Only the addressed account receives an additional object:
{
"invocation": {
"context": [
{"event_id": "019...", "offset": 41, "kind": "message", "text": "..."}
]
}
}The server selects the zero-to-ten retained events allowed by the target's
current channel grant immediately preceding the invocation, in ascending
order. The ready-frame limit of ten is a protocol maximum, while each
automation roster entry reports its effective invocation_context_events.
The server excludes the target's own messages.
Bot/agent-authored context is included only when the channel's
automation-chatter switch is currently enabled. Context events cannot contain
nested invocation objects. This bounded release is authority supplied by
invocation.receive; it does not silently grant history.read or a passive
live feed.
Retained invocation targeting and native provenance are committed atomically.
An invocation-only agent can reconnect and resume missed addressed events from
its app-key checkpoint while unrelated history remains absent. Delivery
rechecks the current grant before selecting context. Retention pruning deletes
the private target relation and provenance before leaving the ordinary
attribution-free cursor tombstone; live_only rooms persist neither relation.
The operation ID hashes the canonical (conversation, target, text) tuple, so
an exact retained retry returns the original event and a conflicting reuse is
rejected without another broadcast.
{
"type": "automation.commands.set",
"request_id": "019...",
"conversation_id": "019...",
"expected_revision": 0,
"commands": [
{
"name": "latest",
"usage": "<topic>",
"description": "Find the latest item for a topic"
}
]
}This operation atomically replaces one automated account's optional command
catalogue in one joined channel. An empty commands array clears it. Command
names omit the leading !, are ASCII lowercase after normalization, contain
only letters, digits, underscores, or hyphens, and occupy at most 32 bytes.
usage is optional and occupies at most 80 UTF-8 bytes; the required
description occupies at most 160. Neither text field accepts control or
bidirectional-formatting characters.
Publication requires an automated-account app key, an attenuated ticket that
allows automation.commands.set, current membership in that channel, and a
channel grant containing live.human.read. The catalogue is discovery
metadata only: it grants no authority, routes no input, and does not execute a
command. A leading !latest topic remains an ordinary PRIVMSG visible to all
clients and reaches the automation only through its independently granted live
event stream.
expected_revision provides compare-and-set replacement. The response status
is applied, unchanged, or stale; exact replacement retries are
unchanged, while a stale response returns the current catalogue and revision
so the publisher can decide whether to retry:
{
"type": "result",
"api_version": "1",
"request_id": "019...",
"operation": "automation.commands.set",
"status": "applied",
"result": {
"kind": "automation_commands",
"conversation_id": "019...",
"revision": 1,
"snapshot_revision": 4,
"commands": [
{
"name": "latest",
"usage": "<topic>",
"description": "Find the latest item for a topic"
}
],
"changed": true
}
}A bot may declare at most 32 commands, and one channel may contain at most 128
commands across admitted automations. Invalid declarations return
invalid_command_catalogue; aggregate exhaustion returns catalogue_full.
Missing publication authority returns scope_denied, and a session that has
not joined the target returns not_joined. Removing the account's channel
grant, or narrowing it to remove live.human.read, removes its catalogue.
Human IRC clients may negotiate the custom telex/bot-commands capability,
which depends on batch and message-tags. The server sends the complete
channel snapshot on JOIN and after every applied replacement as one
BATCH telex/bot-commands; each TELEXCOMMAND entry carries the account,
command name, and base64url-encoded usage and description. A receiver replaces
its prior snapshot only after the closing BATCH. Clients that do not
negotiate the custom capability receive no catalogue traffic.
{
"type": "content.search",
"request_id": "019...",
"conversation_id": "019...",
"query": "event plan",
"cursor": null,
"provider_id": null,
"limit": 20,
"bulk": false
}The session must be joined and the ticket must allow content.search for that
channel. Humans use ordinary current channel admission. Automated callers also
need content.consume; bulk search additionally requires content.bulk.
Search, query rate, package visibility, active provider state, and optional
provider filtering are evaluated by the serialized store operation. A result
has kind: "catalogue", bounded structured descriptors, next_cursor, and
the echoed bulk flag. Named automation lists and provider request-base URLs
are never projected.
Individual pages permit at most 25 results and bulk pages at most 50. A page is shortened with a valid continuation cursor when maximal descriptor escaping would otherwise exceed the 128 KiB server-frame ceiling.
{
"type": "content.request",
"operation_id": "019...",
"conversation_id": "019...",
"provider_id": "019...",
"package_id": "event-plan-2026",
"bulk": false
}content.request requires the same joined/ticket scope and independently
checks visibility, consumption, requester kind, current automation grants,
provider/package bulk opt-in, per-hour request and claimed-byte quotas, and
outstanding capability count. It returns kind: "content_capability" with the
same descriptor, a five-minute provider URL, expiry, and the explicit
network-address/provider warning.
The URL is requester-private, unretained, and never broadcast as an event. Its
ct1 token has 256 random bits; only a SHA-256 digest is stored. Exact
operation retries return duplicate and a null URL without charging or
minting again. The provider consumes the token through the authenticated
control redemption endpoint. See external content references
for the assertion, privacy boundary, legacy XDCC presentation, and fixed
budgets.
An automated provider uses content.package.upsert after joining its registered
local channel:
{
"type": "content.package.upsert",
"request_id": "019...",
"conversation_id": "019...",
"package_id": "event-plan-2026",
"display_name": "Annual event plan",
"description": "Planning notes and print assets",
"filename": "event-plan-2026.tar.zst",
"byte_size": 7340032,
"media_type": "application/zstd",
"sha256": null,
"available_until_ms": null,
"visibility": "human_members",
"consumption": "human_members",
"bulk_enabled": false,
"named_automation": []
}The ticket must allow content.package.upsert; the store rechecks that this
automated account owns an unpaused provider registration and still has
message.publish. The result returns the normalized descriptor. A package
using human_members_and_named_automation in either policy must supply one to
32 unique active bot/agent names that currently have content.consume; the
list is policy input and never catalogue output.
content.package.remove takes request_id, conversation_id, and
package_id, and returns removed or unchanged. Removing a package also
removes its unexpired capabilities by channel-owned cascade.
An automated provider with a registered DCC endpoint may request
content.transfer. After it joins the registered channel, an authorized legacy
XDCC SEND produces this private server push to one eligible provider session:
{
"type": "content_transfer_request",
"api_version": "1",
"transfer_id": "019...",
"conversation_id": "019...",
"provider_id": "019...",
"package_id": "event-plan-2026",
"filename": "event-plan-2026.tar.zst",
"byte_size": "7340032",
"sha256": null,
"dcc": {
"advertised_ipv4": "203.0.113.20",
"first_port": 45000,
"last_port": 45015
},
"expires_at_ms": 1753000030000
}The session either opens one listener and offers an approved port:
{"type":"content.transfer.offer","transfer_id":"019...","port":45000}or declines it:
{"type":"content.transfer.decline","transfer_id":"019..."}An accepted offer returns a correlated result, atomically consumes the
capability, and causes Telex to emit conventional CTCP DCC SEND to the exact
requesting IRC connection. The provider and IRC client then exchange bytes
directly; no file data crosses this WebSocket or the IRC daemon.
If that client sends conventional DCC RESUME, the assigned provider session
receives:
{
"type": "content_transfer_resume",
"api_version": "1",
"transfer_id": "019...",
"position": "1048576"
}After preparing the file stream at exactly that offset, it replies:
{
"type": "content.transfer.resume.accept",
"transfer_id": "019...",
"position": "1048576"
}Telex then sends the matching CTCP DCC ACCEPT. Byte counts and resume
positions are decimal strings where projected by the server so JavaScript
clients retain full unsigned 64-bit precision. After the transfer ends, the
provider should release the ephemeral mapping:
{"type":"content.transfer.finish","transfer_id":"019..."}The pending offer lasts 30 seconds and an accepted resume mapping lasts at most one hour. The exact provider or requester connection closing removes it sooner. An endpoint address and at most 256 inclusive ports are operator registration policy; the provider can select only one port from that range.
The server sends ready, result, event_batch, history_page,
roster_page, event, automation_policy, content_transfer_request,
content_transfer_resume, and error application messages. Results are
requester-private control traffic. They are not retained events,
direct-message conversations, or replayable social edges.
An applied grant, chatter, guardrail, or pause mutation sends a full replacement to every joined, in-scope native session. A newly started cooldown sends the same replacement without advancing the durable revision:
{
"type": "automation_policy",
"api_version": "1",
"conversation_id": "019...",
"policy_revision": 8,
"automation": [],
"automation_chatter_enabled": false,
"automation_guardrails": {
"paused": true,
"window_seconds": 60,
"account_message_budget": 60,
"account_byte_budget": 32768,
"channel_message_budget": 240,
"channel_byte_budget": 131072,
"cooldown_seconds": 30,
"repeated_content_limit": 3,
"causal_signature_limit": 2,
"channel_cooldown": null,
"account_cooldowns": []
}
}The bounded roster uses the same participant objects as channel.join. A
lagged policy receiver closes with policy_refresh_required; continuing with a
silently stale processing disclosure is not permitted.
WebSocket ping/pong frames provide transport liveness. A lagged native event receiver is closed with a resumable error instead of silently dropping events; the client reconnects and resumes from its durable checkpoint. Duplicate delivery before acknowledgement is expected and safe.
Content reset remains a stopped-service operation. Every new ready frame
therefore reports the post-restart content epoch; a client that retained an old
epoch discards its old conversation map before joining and resuming again.
The dependency-free ECMAScript library exposes NativeSession and
NativeDurableSync. It bounds and validates ticket/server frames, creates
canonical UUIDv7 request and operation IDs, correlates requester-private
results, and commits each history page or live event to the existing file or
IndexedDB store contract before acknowledging its last event. Storage failure
closes the stream without advancing the checkpoint. It also validates and
stores the complete automation snapshot, accepts only nondecreasing policy
replacements, distinguishes bot and agent event authors, validates bounded
non-nested invocation context, and emits an automationpolicy client event.
The terminal renders purpose, processing boundary, exact grants, history
window, invocation-context allowance, data handling, chatter state, guardrail
limits, pause, and cooldowns
before restoring cached messages; its /invoke ACCOUNT text command exercises
the visible invocation operation. The same library validates catalogue
descriptors and capabilities, publishes provider packages, keeps 64-bit package
sizes as decimal strings, and never fetches a returned URL. The terminal's
/catalog [query] and /get PROVIDER-ID PACKAGE-ID commands show provider
disclosure and the address warning before printing the one-use URL.
NativeSession.setAutomationCommands() publishes a revisioned atomic command
catalogue for an admitted automated account. The shared IRC session negotiates
telex/bot-commands, validates each bounded snapshot batch, and emits one
automationcommands event only after the closing batch. The bundled web client
uses that event for first-token ! completion without adding catalogue frames
to chat history or the server console.
The small native terminal client exercises this vertical slice against the real daemon. The richer terminal and static web clients continue to use IRC/IRC-over-WebSocket for DMs, presence, moderation, and the complete compatibility surface. Each human installation retains its own opaque cursor token; using one token in two adapters means sharing one server checkpoint regardless of their separate local caches. Automated deployments receive the same isolation from distinct app keys.
The reference MCP adapter attenuates its one-use
ticket to the six JOIN/resume/history/roster/ack/send operations and its explicit
channel list. Its private durable inbox retains the complete validated invocation
envelope before acknowledgement, while MCP resources filter cached event
classes against the current grant and guardrail state. MCP tools still submit
ordinary message.send operations, so the same delivered-cause proof,
idempotency, budgets, loop suppression, pause, and cooldown checks apply.
- First application frame: five seconds.
- Inbound application frame: 16 KiB.
- Outbound application frame: 128 KiB.
- Joined channels: 32, shared with the IRC runtime limit.
- History page: 1 through 100 events.
- Current roster page: 1 through 100 active nicknames.
- Pending session tickets: 256 for 30 seconds.
- Ticket channel allowlist: 32; operation set: thirteen fixed v1 names.
- Automation roster: 32 accounts per channel; delivered causal-proof window: 1,024 event IDs per native session; invocation context: 0 through 10 retained events per channel/account grant.
- Automation command catalogue: 32 commands per automated account/channel and 128 across one channel; command name: 32 bytes; usage: 80 UTF-8 bytes; description: 160 UTF-8 bytes.
- Automation operating window: one minute; account maximum: 600 messages and 512 KiB; channel maximum: 10,000 messages and 8 MiB; cooldown: one second through one hour; repeated content: fourth equal normalized message; causal loop: third equal signature within at most 16 ancestors.
- External catalogue query: 128 bytes; individual page: 25 descriptors; bulk page: 50 descriptors; capability lifetime: 300 seconds; provider catalogue: 10,000 packages; DCC offer: 30 seconds; active DCC resume mapping: one hour; registered DCC range: at most 256 IPv4 ports. The fixed per-hour requester budgets are documented in external content references.
- Store and outbound queues: existing bounded daemon queues.
- Protocol, history, message, and administration commands: the corresponding configured token buckets, with visible typed throttling errors.
- A ticket theft permits one connection for at most its remaining short lifetime and only its echoed effective access. Tickets are never logged, persisted, placed in URLs, or reusable.
- TLS protects credentials, tickets, frames, and content in transit to this server. The server and its operator can read retained plaintext; this is not end-to-end encryption.
- Account disable, human password change, and live channel revocation remove native sessions through the same runtime reconciliation as IRC clients. Automation app-key revocation blocks later ticket issuance.
- Authorization occurs atomically inside retained-history and publication store operations and again before each live event. No adapter receives a raw database handle.
- A native session cannot observe a channel merely because it knows a conversation ID. It must pass current admission and remain joined.
- Automation receives the narrower view selected by its current per-channel grant. Possession of an automation app key or unrestricted ticket cannot override that durable authority.
- Automation budgets are serialized with event append and shared across every app key for the account and room; a reconnect cannot reset them. Transient loop digests and idle counters age out through ordinary maintenance.
- Content search/request budgets are likewise serialized across installations. Gated descriptors contain no provider URL; capability tokens are private, digest-only at rest, and redeemable once by the authenticated provider. Direct provider HTTPS or DCC still reveals the downloader's address to that provider. DCC also reveals the provider's registered address and ordinarily does not encrypt the byte stream.
- The daemon can stop future release after revocation but cannot erase content already retained by a client, model provider, or external tool.
- Re-expressing every IRC moderation, presence, DM, and discovery command.
- Hidden passive subscribers or unauthenticated webhooks.
- Cookies, OAuth providers, raw database queries, or arbitrary event filters.
- Model hosting, prompt/tool execution, provider byte hosting, or media proxying.