NEOTH exposes the same buddy through multiple surfaces. Channels are not second-class prompt pipes: they share profile memory, recall, skills, policy, redactions, provider routing, and audit.
| Channel | Best for | Transport |
|---|---|---|
| GUI | Normal users, setup, memory review, privacy controls. | Native Slint app. |
| CLI | Operators, SSH, scripts, coding sessions. | Local process. |
| Telegram | Fast personal phone access. | Bot API long polling. |
| WhatsApp Business | Mainstream phone access. | Meta Business Cloud API webhook. |
| WhatsApp Web (Baileys) | Operator-owned personal/dedicated account access. | Repository-owned Node sidecar, authenticated HTTP long-poll. |
| Keet-identity private topic | Private peer-to-peer conversations between NEOTH companions. | Repository-owned Pear/Hyperswarm companion over an authenticated loopback bridge; not an existing Keet app room. |
| Slack | Workspaces and team workflows. | Socket Mode WebSocket + Web API. |
| Discord | Community and DM usage. | Gateway WebSocket. |
| Signal | Private personal messaging. | Local signal-cli daemon (JSON-RPC/REST poll). |
| Matrix | Federated encrypted rooms/DMs with explicit invite policy. | matrix-sdk (matrix-channel feature, persistent crypto/session store). |
| LINE | Mainstream phone access (Asia). | Messaging API webhook + push REST. |
| IRC / Twitch | Ops channels, streams. | irc crate, dial-out TCP (irc-channel feature). |
| Mattermost | Self-hosted team chat. | WebSocket + REST, dial-out. |
| Nostr | Decentralized encrypted DMs. | NIP-17 via relays (nostr-channel feature); durable restart catch-up cursor with event-ID de-dup. |
| iMessage | Apple-ecosystem messaging. | BlueBubbles server on a Mac, REST poll (dial-out). |
| Google Chat | Workspace orgs. | GCP Pub/Sub PULL subscription, no public URL (gchat-channel feature). |
| Local inbox triage and quarantine. | Source-build imap_fetch feature; IMAP over TLS with app-password or XOAUTH2. No SMTP/send path. |
|
| Calendar | List or create schedule items with approval. | CalDAV collection over authenticated WebDAV. |
Untrusted-audience readiness. Six adapters — IRC/Twitch, iMessage (BlueBubbles), Mattermost, Google Chat, Matrix and Nostr — fail closed on their named inbound identity policy but still need the common operator/sender/conversation gate before they are suitable for an untrusted audience. Twitch additionally authenticates only the bot transport. Settings includes setup fields for these adapters, including their required sender identities where enforced. Native GUI and real-provider acceptance remain separate. See the release notes for the open items.
Every channel should pass through:
- identity mapping
- allowlist or account binding
- ingress sanitizer
- profile approval gate
- provider destination audit
- outbound send policy
- WAL event trail
Settings → Channels exposes the required allowed sender for Slack, WhatsApp Business, Discord, Signal, BlueBubbles iMessage, Mattermost, Google Chat and Nostr. These values accompany the credentials during Add/Configure and are required by the existing private setup request. An empty value stops setup before any child is launched; no permissive default is created.
Use the platform identity requested by the field: Slack U/W user ID, numeric Discord user ID, E.164 Signal number, Google Chat users/, or Nostr public key. The channel-specific CLI validators remain authoritative. Existing saved credentials are not displayed while editing. Native setup and provider probes remain required for release qualification.
BlueBubbles also exposes optional watched chat GUIDs in Settings. Enter a comma-separated list to restrict accepted chats to those GUIDs. Blank removes an existing chat filter on save; the required allowed-sender policy still applies. This uses the existing private setup request and the adapter's existing chat allowlist, without discovering chats or displaying saved credentials.
The daemon binds each inbound handler to a canonical channel and account at adapter startup. A message cannot choose a different binding. Identity aliases, inbound session keys, media references, rate-limit buckets and channel-originated lease subjects include that binding.
Existing identity aliases remain visible as legacy-unbound in
neoth identity list. They are not automatically assigned to default.
The admitted legacy Telegram singleton can retain a configured operator UUID
only when its exact sender/chat alias already matches the operator pin. Other
aliases require an explicit identity merge to unify their account-qualified
identities. Merge keeps the original migration claim receipt unchanged.
Use the exact account-qualified subject when granting a channel send or MCP lease. For example, in PowerShell:
$subject = neoth lease channel-subject telegram default 123456789
neoth lease grant $subject channel_send --ttl 1h
neoth permissions check channel_send --subject $subjectchannel-subject validates the channel and account and prints the subject; it
does not create a lease or access credentials. With --output json it returns
subject and channel_ref. Channel-originated MCP requests use the same
subject with a tool-specific lease such as mcp_tool:server:tool.
Existing bare-sender leases do not cover these account-qualified requests;
regrant any needed lease using the helper. A grant for one account cannot
authorize the same sender on another account.
Telegram is the current account-map surface. Each configured account has an
explicit public allowed_user_id and a matching credential token. The token is
either present in the credentials file or represented there by a null
placeholder whose account-specific keychain value is loaded at runtime. A
partial, mismatched, blank, or zero-ID map is rejected; legacy scalar Telegram
fields cannot coexist with a map.
Migrate an admitted legacy Telegram singleton with:
neoth channel migrate-legacy telegram --account <account-id>The command commits the paired policy and credential migration before cleanup. If later cleanup fails, retrying completes cleanup against that committed pair. Its output never prints token material.
While a Telegram account map is active, the legacy flat channel add telegram,
channel remove telegram, and channel set-credentials telegram mutations are
rejected. Reload compares account-qualified credentials and restarts only the
changed Telegram adapter. Replies remain with the adapter that owns the inbound
account; flat proactive Telegram and Cron Telegram announcement paths refuse to
send while a map is active.
Named Telegram accounts can now be added or replaced one at a time with
neoth channel account add telegram --account <id> --telegram-user-id <id> --token <secret>. The account-specific set-credentials form reads a strict,
account-bearing private-stdin JSON envelope; it accepts no unknown fields. Both
paths probe the candidate's exact account before committing the paired
configuration/credential update. A changed relevant file or named keychain
value produces a retry error instead of overwriting newer account state; output
is secret-free. The established legacy singleton must still be migrated
explicitly before a map is added.
Settings → Channels offers Add account for a valid map and for the exact fresh Telegram state, alongside the explicit legacy Configure choice. Each account row provides Edit, its exact-account Test, and Retire with an explicit confirmation for the displayed account ID. A malformed map is repair-only in the CLI; the GUI neither repairs it nor silently migrates it. The UI's account request stays in private stdin and accepts only the exact secret-free acknowledgement for that account.
The GUI retirement surface uses the existing explicit CLI transaction. Other account families and remaining account-aware outbound routing are separate P1-16 work. Native GUI acceptance and full CI remain required.
Pairing is disabled until an operator explicitly enables it for one mapped Telegram account:
neoth channel account set-dm-pairing telegram --account <account-id> --enabled
neoth channel pairing list telegram --account <account-id>
neoth channel pairing approve telegram --account <account-id> --code <code>
neoth channel pairing dismiss telegram --account <account-id> --request-id <id>Settings also displays each mapped account's configured DM-pairing state and
offers an account-specific enable/disable confirmation. Enabling accepts
eligible pairing requests for later approval; it does not automatically
authorize a sender. Disabling blocks new pairing requests. Pairing and retirement
controls wait while either account operation is in flight. The inventory refreshes
only after a successful CLI exit and an exact-account receipt matching the
requested policy; an unconfirmed result preserves the displayed state.
The corresponding CLI disable command omits the presence flag --enabled.
Pending-request list/approve/dismiss remains in the CLI. This new GUI surface
still requires native and provider acceptance.
Only a private, non-pinned direct-message sender can create a pending request.
Requests are scoped to that account and its current binding, expire after one
hour, and are capped at three. Pinned operator groups and the established
ConfirmBus path keep their existing behavior. Edited Telegram messages cannot
create or refresh pending pairing requests. A binding rotation invalidates only
that account's pending state;
another mapped account remains separate. A missing authenticated mapped receipt
is surfaced as a failure and does not continue into the chat pipeline.
The historical W29 CLI selection, Clippy, build, AccountConfigContracts, formatting, CLI-doc generation, final GUI check, and integrity checks passed. This feature does not provide pairing migration, physical-provider live acceptance, or complete multi-account readiness.
The admitted W32 surface adds explicit retirement for one mapped Telegram account:
neoth channel account remove telegram --account <account-id>The command never assumes a default account. A later re-add of the same visible
name receives a fresh internal UUID, so existing leases, pending pairing
requests, and queued work remain tied to the retired account. Historical records
with no UUID remain compatible only when the current account remains in that
same historical form; they are never silently relabelled as the replacement.
Settings → Channels exposes that same operation as Retire on each valid
mapped account row. Confirm shows the exact visible account ID; no default or
sole-account fallback is inferred. Only a successful process result with a
strict acknowledgement naming that Telegram account and removed: true
allows the GUI to refresh the account list. An error or mismatched response
leaves the operation unconfirmed and does not trigger an automatic retry.
The GUI selects the public account ID; it does not add an internal UUID-bound
confirmation contract to the existing CLI authority.
Validation is pending, and P1-16 remains open.
SelfStage is an opt-in verified staging path. The manual command
neoth update --self --apply performs the binary swap. Self-probe and
SelfStageAllowed apply only to this lane; CLI, npm, Git, OSV, installer, and
Skill update lanes remain denied. Production runtime execution is pending the
proof that an enabled outer/helper route produces the required ordered receipts.
R3-18B remains open.
W34 adds sealed startup provenance for legacy Telegram and Slack live delivery.
W262 extends that closed default-live family to Discord Gateway replies only:
the adapter starts only with its bot token, normalized immutable allowlisted
sender snowflake, and provider, then receives the exact discord/default
provenance. Mapped Telegram remains separate. This limited source-admitted slice
does not establish Discord account-map or proactive provenance, provenance for
other channels, or close P1-17.
The OpenClaw importer accepts --config, --source-account, --account, and
--telegram-user-id to create one explicitly named target account from one
selected source account. It reads the selected token only in memory, probes the
prepared target before mutation, rechecks the source set, and then uses the
same prepared CAS commit path as the named-account workflow. Its report uses
shared raw-JSON custody and is canonically redacted. Local validation covers
the final selection (1,492 pass / 0 fail / 1 existing Unix-only ignored),
Compound Clippy, AccountConfigContracts, custody, formatting, and Python
checks. This does not claim a full OpenClaw configuration or transcript
migration, pairing migration, a physical provider live import, or registry
publication.
neoth proactive route --default --channel telegram --account <account-id>
selects an explicit Telegram
account for a new mapped route. It accepts only an exact, non-legacy
authenticated account bundle; a map-active Telegram route cannot omit an
account. The selected account is stored with queued work, so a later routing
default change cannot reroute it to another account.
At physical delivery, the worker holds the existing DeliveryLock and, just
before durable Prepared admission, reloads a fresh coherent config and
credential pair. It resolves the stored account against that pair and builds the
owning adapter from the returned bundle. The recipient is only that bundle's
allowed_user_id; routing data, a queued body, and a legacy recipient cannot
override it.
A credential or policy update completed before this fresh admission applies to the attempt: it uses the current exact account bundle or settles without a send. This does not promise retroactive cancellation once the coherent pair has already been admitted. Under an active map, a missing, unknown, removed, partial, mismatched, or legacy-unbound account fails closed and never falls back to another account. A historical unbound Telegram item remains valid through the existing legacy flat path while the effective map is empty.
New account-bound claims and durable delivery evidence use a version-4 typed channel reference. Historical queue rows, claims, and WAL records retain their existing versions and recovery semantics. P1-16 remains open for other account, transport, pairing, UI, health, and outbound surfaces.
W20 adds a read-only, account-isolated transport-evidence view for two already
authorized paths: mapped nonlegacy Telegram live replies and v4 mapped Telegram
proactive egress. Only the validated TelegramAccountBundle path may attach the
bound account metadata to a live intent; generic/default bindings, the legacy
Telegram singleton, other channels, webhooks, probes, and outboxes remain
unbound.
The evidence reader performs one complete authenticated home-WAL scan. It first validates each bound intent/result relationship and only then derives the 24-hour per-account counters. Incomplete, unavailable, malformed, replaced, or unauthenticated WAL yields no partial counters. The stored typed reference is historical evidence: it is never rebound through current configuration, credentials, runtime health, or a route default.
neoth doctor can surface account-specific transport flapping from those
validated counters. It considers only completed adapter attempts for the failure
rate (initially at least five completed attempts and at least 20% failures).
Armed-without-result and unsettled live intents are explicit inconclusive
evidence and warn without being counted as completed attempts. A scan error is
unavailable rather than a partial report; no observed bound attempts passes the
check.
This is metadata and diagnosis only. It does not create authorization, routes,
credentials, retries, runtime-health authority, or a physical-delivery claim.
W20 now has nine admitted source files and TestBuild01 passes; four real
authenticated-WAL tests also pass. An initial selected run exposed a real legacy
JSON byte-order regression, repaired by restoring the former json! value plus
optional-reference writer shape; the raw-byte assertion remains unchanged.
Doctor's all-check documentation list has 60 entries, while run_all_checks
returns 59 runtime outcomes. The admitted mapped live-auth repair keeps the
existing opaque capability gates and append_authenticated path: CHANNEL_SEND
precedes the terminal marker for success and failure, without changing unbound
payload/order behavior. TestBuild02 PASS (4m07s; 183.27 GiB minimum free;
11.07 GiB peak), Selected02 394/0/0 (before this repair), Python 19+11+8,
and Clippy03 PASS are intermediate evidence. Final TestBuild03 PASS
(3m49s; 202.66 GiB minimum free; 10.65 GiB peak), final selected 397/0/0
(catalogue 14,389), 13 integration targets 157/0/0, and formatting/GUI
lint/self-test pass. W20 is locally verified and published at
080131b4320ac3935d1f5d05e242295e5c3625a8. No current-head full-CI or cross-platform
acceptance is claimed. See
the W20 verification receipt.
The neoth channel family manages the canonical messaging registry (Telegram,
Slack, WhatsApp Business, WhatsApp Web/Baileys, Keet-identity private topics,
Discord, Signal, LINE, IRC, iMessage/BlueBubbles, Mattermost, Google Chat,
Matrix, Twitch, and Nostr) without the full wizard:
neoth channel list # which channels are configured right now
neoth channel add telegram # prompts for token + exact numeric sender ID
neoth channel test telegram # live read-only credential check (no message sent)
neoth channel test telegram --account <account-id> # one mapped Telegram account
neoth channel remove telegram # clear token + sender policyaddprompts for channel secrets with no terminal echo on an interactive TTY and for required public policy fields. Telegram commits its token in~/.neoth/credentials.yamland its exact numeric sender allowlist in~/.neoth/freedom.yamlunder one locked rollback-safe update. Script it withneoth channel add telegram --token "$TOKEN" --telegram-user-id "$TELEGRAM_USER_ID".testvalidates the configured credentials with a protocol-specific, read-only check: TelegramgetMe, Slackauth.test, WhatsApp Business phone-node lookup, Baileys/Keet companion health, Discord bot identity, signal-cli registered accounts, LINE bot identity, BlueBubbles ping, Mattermost current user, Google Pub/Sub subscription access, Matrix/account/whoami, Twitch OAuth identity/scopes, and Nostr relay connection. Whenchannel_routing.jsoncontains an iMessage chat GUID or Google Chat space,test imessageandtest gchatadditionally read that exact target through authenticated BlueBubblesGET /api/v1/chat/:guidor Google Chatspaces.get. The returned GUID/resource name must match the configured destination. A rejected or different target fails the test even when the transport probe succeeds. Without an outbound destination the result remains transport-only; testing never creates a routing file or a relink receipt. It sends no chat, consumes no inbound queue, and publishes no relay event. IRC returns the typedunavailableverdict because registering the configured nick is stateful and could collide with the live adapter. Matrix password-only auth is likewiseunavailable; use a device-bound access token for a safe live probe.- Mapped Telegram
testrequires an exact account: runneoth channel test telegram --account <account-id>. The account is never inferred from an active map, includingdefault; an unknown or invalid map fails before a probe.--accountis rejected for non-Telegram channels and for the legacy scalar Telegram singleton, where the existing no-account test remains the valid path. list/removeshow and clear configured state. All four accept--output json.
For a valid Telegram account map, channel list emits one secret-free child
per configured ChannelRef. This is static readiness: it proves the
matching policy and effective credential bundle are usable, not that an adapter
or daemon is live. A partial or mismatched map remains an error instead of
producing usable-looking account children.
When a matching local daemon instance publishes a current private runtime
projection, the same child can additionally report running,
configured_not_started, failed, inactive, or credentials_invalid.
The projection is accepted only for the exact current account binding and live
daemon instance. A missing, stale, mismatched, unreadable, or unproven
projection is unknown, while the static account readiness stays visible;
JSON omits the optional runtime field in that case. Runtime projection is
observational only and is not credential or authorization evidence.
Settings → Channels shows a Telegram account map as nested, secret-free account rows. Each row separates its static configured state from the optional, read-only runtime observation above. The panel can run the read-only Telegram test only for the exact selected account and refuses to present a result for a different account as that selection. It does not create accounts, pair devices, or offer a migration workflow.
For operator detail, use an explicit account ID:
neoth connect telegram --account ops_bThe map overview lists configured account children but has no implicit
default selection. An invalid or partial map is a repair state: no account is
usable or testable until matching policy, nonzero sender, and credentials are
complete. A legacy no-map Telegram setup retains its compatible no-account
detail and test path.
The W18-19 source is independently reviewed and locally verified: full GUI test-source checking, 175 core and 350 actual GUI parser/action tests, final Clippy, formatting and GUI lint pass. Native rendering and live delivery acceptance remain separate. See the W18-19 verification receipt.
Wave 17 is locally verified by the focused unit and integration checks recorded in its verification receipt. Full cross-platform CI and live Telegram acceptance remain separate.
Messaging-channel credentials live in credentials.yaml or the selected
keychain backend. While neoth serve is running, its reconciler watches the
effective credentials and config generation, debounces replacements, and
restarts only the changed channel. A malformed/unreadable credential store
stops the channel fleet fail-closed; NEOTH never keeps stale tokens active.
Email and calendar do not go through neoth channel add and are
not collected by neoth init: IMAP uses its documented environment/OAuth
inputs, while CalDAV uses caldav_{url,username,password} in
credentials.yaml or NEOTH_CALDAV_*.
add prompts per channel (B9): discord bot token + immutable user ID ·
slack bot/app tokens + immutable member ID · whatsapp Cloud credentials
- exact E.164 sender · signal signal-cli URL + own and allowed E.164 numbers
· line channel access token + exact user ID (+ channel secret for inbound
webhooks; blank = push-only) · irc server host, nick, optional
NickServ password + channels csv · imessage BlueBubbles server URL +
password · mattermost server URL + token · gchat path to the GCP
service-account JSON key + Pub/Sub subscription name. Twelve adapters enforce
a mandatory inbound identity policy: Telegram requires
telegram_user_id, IRC requiresirc_allowed_account, BlueBubbles requiresimessage_allowed_sender, Mattermost requiresmattermost_allowed_user_id, Google Chat requiresgchat_allowed_sender, Nostr requiresnostr_allowed_pubkey, Discord requiresdiscord_allowed_user_id, Slack requiresslack_allowed_user_id, WhatsApp Business requireswhatsapp_allowed_sender, Signal requiressignal_allowed_sender, LINE requiresline_allowed_sender, and Matrix requires at least one ofmatrix_allowed_user_idormatrix_allowed_room_ids. The shared--allowed-senderflag supplies the channel-appropriate single identity; Matrix additionally accepts--allowed-rooms-csv.channel listreports canonical static configuration truth;channel testadds live credential/reachability truth.
Twitch remains the explicit audience-policy exception: it authenticates the bot and configured stream rooms, but does not yet bind inbound chat to an operator identity or mention policy. Do not expose its tool-bearing pipeline to an untrusted stream audience. Transport membership is never treated as operator authorization for the other inbound adapters.
Matrix state-store selection, LINE webhook port, and IRC port/TLS/secondary nick filtering now have CLI and GUI configuration surfaces. Their previously file-only settings retain the same runtime defaults. Native and visual setup acceptance remain required; field exposure alone does not close channel parity.
These keys are documented for existing operators, not presented as a zero-friction setup claim. Generic descriptor-rendered forms, durable recovery across a process crash between OS-keychain and file publication, and persisted multi-account channel identity are still release-blocking work.
- Open Telegram and search for
@BotFather. - Send
/newbot. - Choose a display name and bot username.
- Copy the token.
- Get your numeric Telegram user ID (for example from
@userinfobot). NEOTH requires this exact inbound allowlist and refuses an open bot. - Run:
neoth channel add telegram \
--token "$TELEGRAM_BOT_TOKEN" \
--telegram-user-id "$TELEGRAM_USER_ID"
neoth channel test telegram
neoth serve| Behavior | Detail |
|---|---|
| Identity | Numeric Telegram user IDs are used, not usernames. |
| Groups | The numeric sender allowlist still applies. There is no separate mention-only group mode. |
| Long replies | Split into continuation messages. |
| Streaming | NEOTH can edit an in-progress message where supported. |
| Inbound attachments | Photos, voice, audio, video, documents, stickers, and captions enter the media pipeline when enabled; downloads are capped at 16 MiB. |
| Outbound attachments | Images, MP4 video, MP3/M4A audio, documents, and Telegram stickers use native Bot API methods with MIME/size checks. Long captions fall back to split follow-up messages. |
| Flood control | Short Telegram RetryAfter responses are honored once; longer limits surface to the caller instead of sleeping indefinitely. |
NEOTH uses the official Meta WhatsApp Business Cloud API. No personal-number hacks and no browser automation.
neoth channel add whatsapp
# Non-interactive equivalent includes --allowed-sender +491701234567
neoth channel test whatsapp
neoth serveCredential fields:
| Field | Description |
|---|---|
whatsapp_token |
Meta Cloud API token. |
whatsapp_phone_id |
Phone number ID from Meta Business console. |
whatsapp_verify_token |
Secret used during webhook verification. |
whatsapp_app_secret |
Used for webhook signature verification. |
whatsapp_allowed_sender |
Exact operator phone number. Stored canonically as 7–15 digits; CLI accepts an optional leading +. |
| Behavior | Detail |
|---|---|
| Transport | HTTPS webhook receiver plus Graph API send path. |
| Streaming | WhatsApp has no edit endpoint; NEOTH sends complete replies. |
| Approval | External sends remain policy-gated. |
| Inbound authorization | A valid Meta signature is transport authentication only. The decoded from must also match whatsapp_allowed_sender before dedup or pipeline dispatch; rejection is WAL-audited without message text. |
| Business review | Meta approval can take time; use Telegram while waiting. |
This optional path uses the repository-owned sidecar in
bridges/whatsapp-baileys/. It is not Meta Cloud and never reads or reuses the
Meta token, phone ID, webhook secret, or proactive destination. Baileys is an
unofficial WhatsApp Web client; use a dedicated account and opt in only after
accepting the platform-policy risk.
cd bridges/whatsapp-baileys
pnpm install --frozen-lockfile
export NEOTH_WA_BRIDGE_TOKEN="$(openssl rand -hex 32)"
pnpm start # scan the QR on first start
neoth channel add whatsapp_baileys \
--url http://127.0.0.1:9120 \
--token "$NEOTH_WA_BRIDGE_TOKEN" \
--allowed-sender '+491701234567' \
--allowed-rooms-csv '120363000000000000@g.us'
neoth channel test whatsapp_baileys
neoth proactive route --channel whatsapp_baileys --dest '+491701234567'
neoth serveThe sender allowlist is mandatory. Groups are deny-by-default and require both
an allowed sender and an exact allowed @g.us group JID. The bridge stores QR
auth in its repo-owned atomic Signal-key store (not Baileys' demo multi-file
helper), plus a bounded durable inbound journal, restart dedup state, and
outbound idempotency state in ~/.neoth/whatsapp-baileys-bridge/ (owner-only). NEOTH
stores its own per-account cursor under ~/.neoth/channel-state/; an expired
cursor fails closed instead of skipping unseen messages.
Every bridge request uses a dedicated bearer token. Plain HTTP is accepted only
on loopback; a bridge on another host needs HTTPS with redirects disabled. Text
and media (10 MiB maximum) work inbound and outbound. See
bridges/whatsapp-baileys/README.md for
the user-service unit, recovery procedure, and protocol contract.
Delivery is deliberately at-most-once. NEOTH persists an inbound claim before the provider/pipeline runs, so a crash can lose a reply but cannot replay the same message into a second paid or tool-bearing turn. Outbound unknown outcomes remain permanent pending tombstones until the operator reconciles them; they are never removed by TTL or capacity pruning.
Slack uses Socket Mode so you do not need a public HTTPS endpoint.
neoth channel add slack \
--bot-token "$SLACK_BOT_TOKEN" \
--app-token "$SLACK_APP_TOKEN" \
--allowed-sender U0123456789
neoth channel test slack
neoth serveRequired Slack scopes:
| Scope | Purpose |
|---|---|
chat:write |
Send replies. |
im:history / channels:history |
Read messages. |
im:write |
Start or reply in DMs. |
users:read |
Resolve identity. |
connections:write |
Socket Mode app token. |
slack_allowed_user_id is the immutable U… or W… member ID, never a
display name or email. The daemon refuses token-only Socket Mode startup.
Every decoded event must match that ID before the shared pipeline runs;
mismatches are dropped without a reply and recorded as the metadata-only
CHANNEL_GATE_REJECTED WAL event.
Discord stores its bot token and exact inbound-user policy in credentials.yaml
(discord_bot_token + discord_allowed_user_id). Configure both through the
CLI or the same registry-driven GUI form:
neoth channel add discord \
--token "$DISCORD_BOT_TOKEN" \
--allowed-sender 123456789012345678
neoth channel test discordThe allowed sender is the immutable numeric Discord user ID, not a mutable
username. neoth serve refuses to start the Gateway receive loop when the
policy is missing or blank. neoth channel test discord makes the read-only
Discord GET /users/@me identity probe without creating a message.
Discord notes:
| Behavior | Detail |
|---|---|
| Gateway | Uses Discord Gateway WebSocket. |
| Message content | Requires Message Content Intent. |
| Formatting | CommonMark-ish with Discord length limits and splitting. |
| Inbound authorization | Every non-bot message must match discord_allowed_user_id before the shared pipeline runs. Mismatches are dropped without a reply and audited as CHANNEL_GATE_REJECTED; no message text enters that audit event. |
Signal uses a local signal-cli HTTP daemon. Configure the registered NEOTH
number and the one operator number allowed to drive inbound turns:
neoth channel add signal \
--url http://127.0.0.1:8080 \
--phone +491701111111 \
--allowed-sender +491702222222signal_allowed_sender is mandatory E.164. The poll loop is not constructed
without it. Envelopes from any other source are dropped before the pipeline and
WAL-audited; delivery receipts, typing events and sync envelopes remain
non-actionable.
IRC requires an authenticated IRCv3 services account in addition to the server
and bot nick. --allowed-sender selects that account; an optional nick filter
cannot replace it:
neoth channel add irc \
--server irc.example.org \
--nick neoth \
--allowed-sender operator-account \
--channels-csv '#neoth' \
--irc-port 6697 \
--irc-tls true \
--irc-allowed-nick operator-nickThe Port, TLS and Secondary allowed nick controls in the IRC form
configure those same fields. Ports must be integers from 1 through 65535.
TLS has three choices: retain current/default, explicitly enabled, or explicitly
disabled. An omitted flag or retain-current choice preserves an existing
setting; it never silently turns an explicit false back into a default.
Blank port/nick fields preserve an existing override during credential updates.
With no prior configuration, the runtime defaults to port 6697, TLS enabled,
and no additional nick filter. Nonblank nick filters cannot contain whitespace
or control characters. Channel removal clears the IRC configuration, including
settings-only state. The adapter reads these settings when it starts; saving
credentials does not claim a live connection reconfiguration.
LINE outbound push can operate with an access token alone, but inbound startup
requires the channel secret and one immutable U… user ID:
neoth channel add line \
--token "$LINE_CHANNEL_ACCESS_TOKEN" \
--password "$LINE_CHANNEL_SECRET" \
--allowed-sender U0123456789abcdefUse --line-webhook-port 9443 with the command above, or the optional
Webhook port field in the GUI, to select a listener port from 1 through
65535. Zero and out-of-range values are rejected. An omitted flag or blank
GUI field preserves an existing custom port during credential updates; a new
configuration without a port uses 8444. The listener still binds only to
127.0.0.1, behind your reverse proxy. Saving the setting does not by itself
rebind a running listener; it applies when the channel starts.
The webhook must pass X-Line-Signature verification and the decoded source
must equal line_allowed_sender. Signature validity alone never authorizes a
turn. Missing/invalid policy keeps the listener off; rejected senders are
recorded as metadata-only gate events.
Matrix is opt-in at build time because the adapter includes the Matrix E2EE and
SQLite crypto-store stack. It requires Rust 1.93 (default builds that omit
matrix-channel remain supported on Rust 1.91):
cargo build -p neoth --features matrix-channel
neoth channel add matrix \
--url https://matrix.example.org \
--nick @neoth:example.org \
--token "$MATRIX_ACCESS_TOKEN" \
--allowed-sender @operator:example.org \
--allowed-rooms-csv '!private:example.org,!ops:example.org'
neoth channel list
neoth serveTo route queued proactive items into Matrix, configure the operator-owned room and a source/default route explicitly:
neoth proactive route --channel matrix --dest '!ops:example.org'
neoth proactive route --source coding_session --channel matrix--password is the first-login fallback when no token is supplied. A configured
access token takes precedence and is verified with /account/whoami; the
returned user and device id are used to restore the exact device session. A
homeserver that omits the device id is rejected because inventing one would
break E2EE continuity. The resulting session is stored atomically at
~/.neoth/matrix_store/neoth-matrix-session.json inside the owner-restricted
crypto-store directory. Tokens and passwords are never printed by probes or
errors.
Choose a different crypto/session directory with neoth channel add matrix --matrix-store-path "C:\Matrix state" alongside the usual account and policy
arguments, or use the optional State store path field in the GUI's Matrix
setup. Both save matrix_store_path through the existing channel configuration
operation. A nonblank path replaces the configured value; omission or a blank
field preserves an existing custom path when updating credentials. If no path
has ever been configured, the default is matrix_store beneath the selected
NEOTH home. Configuring a path does not move or migrate an existing E2EE store;
choose the directory containing the account's intended session state.
Matrix policy is fail-closed at the boundaries:
| Field / flag | Behavior |
|---|---|
matrix_allowed_user_id / --allowed-sender |
Restricts inbound senders and inviters to one Matrix user id. |
matrix_allowed_room_ids / --allowed-rooms-csv |
Restricts joins, inbound messages, and proactive sends to the listed room ids. |
| Both allowlists set | Both must match. One allowlist cannot bypass the other. |
| Neither allowlist set | Matrix is not started. Existing-room messages would otherwise bypass the invite-only gate, so an open adapter is refused. |
matrix_require_encryption |
Defaults to true, including old config files without the field. Plaintext rooms are neither read nor written. |
--allow-plaintext |
Explicitly writes matrix_require_encryption: false; channel list/doctor surface the plaintext posture as a warning. |
The adapter checks room encryption after an allowed join, before inbound text
enters the pipeline, and before every reply/proactive send. If encryption state
cannot be verified while enforcement is enabled, the operation is refused.
The proactive dispatcher uses the same credentials, persistent device store,
room allowlist, and encryption policy; it never accepts a destination from the
queued item itself. In a binary without matrix-channel, the saved Matrix route
remains sidecar-only and is never presented as a live delivery path.
Configured Matrix credentials in a binary without matrix-channel are reported
as an error and neoth serve logs that the adapter was not started; they are
never reported as live.
Desktop release archives include neoth-keet-bridge, NEOTH's repository-owned,
full-duplex text companion. It uses keet-identity-key for portable sender
identity and Pear/Hyperswarm building blocks for an encrypted private topic. The
standalone includes its Bare runtime, so normal installs need neither Node.js nor
the Pear CLI.
Run setup once and keep the printed bearer token and nk1_... topic private:
neoth-keet-bridge setup
neoth-keet-bridge serveOn every peer, join the same topic and exchange only the printed self_id
values. Then wire the local companion interactively with neoth channel add keet, or non-interactively:
neoth channel add keet \
--url http://127.0.0.1:9130 \
--token '<local companion bearer_token>' \
--server 'nk1_<shared topic capability>' \
--allowed-sender '<remote self_id>[,<another remote self_id>]'
neoth channel test keet
neoth serve--server is the generic channel CLI field that carries the Keet topic. The
allowlist is mandatory and exact/case-sensitive. The live probe requires an
authenticated protocol/version handshake, both send and receive capabilities,
the configured joined topic, and its high-water cursor; static credential
presence alone never reports the channel healthy. Runtime receive starts at
that high-water cursor, so first startup does not replay arbitrary older local
history. Sends are durable and idempotent, and inbound sender IDs come from
verified Keet identity attestations before the allowlist and NEOTH pipeline.
This does not automate or read existing Keet desktop/mobile rooms. Keet has
no supported public room/message API, so NEOTH creates its own private
Keet-identity Pear/Hyperswarm conversation instead of guessing a proprietary
protocol. The old keet_seed_phrase field remains ignored and --seed is
rejected. neoth channel remove keet clears both the real companion credentials
and any legacy seed state. The separate NEOTH cluster mesh likewise remains a
different protocol.
Companion operations, recovery, exact HTTP contract, and threat boundary are in
bridges/keet/README.md.
Email is treated as a sensitive channel because it contains other people's text, attachments, tracking links, and prompt-injection bait.
Network-live email requires a source build with imap_fetch; the named release
bundles intentionally omit that feature. Configure NEOTH_IMAP_USERNAME plus
an app-password or the documented Gmail OAuth inputs, then use
neoth email fetch. The command reads bounded UNSEEN messages with
BODY.PEEK[], so triage does not mark mail read. --dry-run remains available
on builds without the feature. Email is not a bot token and does not go through
neoth channel add or neoth init.
Default behavior:
| Action | Default |
|---|---|
| Fetch inbox | Explicit command, or the separately opt-in email-ingest cron; both require imap_fetch. |
| Message state | Non-destructive BODY.PEEK[]; no read/delete/move mutation. |
| Triage | Sanitizer, deterministic threat score, trusted-domain annotation, and fail-closed quarantine. |
| LLM tie-break | Off by default because it can spend a provider call; failures preserve the conservative verdict. |
| Send or draft replies | Not implemented. There is no SMTP client or email-send path. |
| Attachments | Parsed within bounded message limits and subjected to the same hostile-content posture. |
Calendar is configured through caldav_url, caldav_username, and
caldav_password in ~/.neoth/credentials.yaml, or the corresponding
NEOTH_CALDAV_* variables. neoth calendar list is read-only;
neoth calendar add performs the external write.
Default behavior:
| Action | Default |
|---|---|
| List VEVENTs | Read-only after credential configuration. |
| Add VEVENT | Requires the calendar.writes_enabled rail, the external-task-write gate, and confirmation unless already granted; emits calendar audit events. |
| Update/delete VEVENT | Not part of the current neoth calendar CLI surface. |
| Extract dates from email | No automatic email-to-calendar write path is claimed. |
NEOTH does not silently merge identities. If you talk to it from Telegram and Slack, those stay separate by default — each channel is its own conversation surface with its own consent gate.
When you DO want recall and profile to follow you across surfaces, link them deliberately:
neoth identity list # every channel identity NEOTH has seen
neoth identity merge <keep> <fold> # link two identities (audited, reversible)The merge is operator-driven, written to a 0x9B IDENTITY_MERGED WAL frame
(reversible tombstone — see privacy.md), and the alias map never
leaves the machine. Without an explicit merge, channels stay independent — the
fail-closed default.
| Check | Why |
|---|---|
| Allowlist enabled | Prevents random accounts from using your buddy. |
| Webhook signatures verified | Prevents spoofed inbound events. |
| Outbound sends gated | Avoids accidental external messages. |
| Attachments size-capped | Prevents resource exhaustion. |
| Ingest sanitized | Prevents prompt-injection and profile poisoning. |
| WAL events written | Makes actions auditable. |
Use live-e2e-protocol.md before trusting a production channel.
Typical smoke:
neoth channel test <channel> # read-only live check or typed unavailable
neoth doctor # full setup diagnostics (incl. channel wiring)
neoth serve
neoth privacy audit --last 1h