Skip to content

Latest commit

 

History

History
503 lines (418 loc) · 27 KB

File metadata and controls

503 lines (418 loc) · 27 KB

Automated participants

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.

Account and trust model

The domain model distinguishes human accounts from automated accounts. Automation has a visible kind:

  • a bot is deterministic automation such as a news feed, bridge, or external content catalogue;
  • an agent may 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.

Provisioning and manifests

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:

  • local runs on the IRC server and names no separate processor;
  • network names a processor elsewhere inside the operator's network; or
  • external names 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.

Channel grants

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.

Disclosure and presentation

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.

Native gateway and adapters

The canonical integration is the authenticated native gateway:

  1. an automated deployment resumes its permitted channel streams from durable delivery checkpoints;
  2. the server emits only events allowed by that account's channel grants;
  3. the external runtime processes an event and submits a message with an idempotency key and, where applicable, a cause event;
  4. 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.

Provenance and guardrails

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.

Privacy, reset, and retention

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.

Initial non-goals

  • 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.

Open design details

  • 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.