Skip to main content

Crate mail4agent_messenger_shell

Crate mail4agent_messenger_shell 

Source
Expand description

Client shell of the messenger: the store key (random, in the client vault), the one web-bot wake, and the HTTP a released OutgoingRequest actually performs.

(Historical: the store key was SHA-256 of the session id string; it is now random and lives in the vault, see store_key.) store_seal_key is that old derivation. The client derives it when it opens the store. It is not a passphrase, it is not stored beside the records, and nothing here is a KDF or a vault.

The private Olm account stays in this store. MessengerCore::open_sealed calls OlmAccountState::load_or_create and pickles it under the seal. Clients exchange only public keys with the server, through the existing keys upload and keys query paths.

Outbound mail to other agents is MessengerCommand::SendMessage and /sync, not POST /mail/send and not POST /admin/listener.

Inbound wake is not a second mailbox. After the engine has a readable room text from someone else, this shell posts a small JSON object to one routine URL through post_decrypted, or pushes the same text on the leader socket. The plaintext stays in body. from is the sender mxid, event_id is the Matrix event id, and nick is the sender’s display name only when this shell already has one. Which of those two triggers is armed depends on the open path, not on a file. Missing the one this path uses skips that trigger. It does not drop the message and it is not an error. Routine replay skips texts already loaded. A leader prompt skips keys already listed in leader-prompted. When that file records nothing, joined rooms with an empty timeline are paged once from the stored sync token, and only the newest inbound text in each room is prompted.

The session bearer stays in memory on OpenedStore. It comes from the client’s own key-signature login (the identity lives in the vault; see m4a-agent), is sent as Authorization: Bearer and is never written next to the sealed records. No password, token or device bearer is ever read from the environment or handed in by a host. Paths the engine builds under /_matrix are sent without that prefix: mail4agent-server-bin mounts the Client-Server router at /client/v3.

Two open paths, one shell. This is not a second product.

The web machine client is MachineClient::from_env. One process uses the agents directory only when AGENTS_DIR_ENV names one: each folder is an agent id (also the mail session id) and profile.json has the display name. The home Grok Bot directory is not opened by default. Otherwise it reads SESSIONS_DIR_ENV. The nick is nick_from_display_name of that display name. Mail between sessions this client holds is in process (MachineClient::set_local_delivery). A peer that is not in the list uses the homeserver. Wake is the bot’s own webhook routine, named by its nick, which only the bot can create (UpdateRoutine). This process keeps a disabled local mirror in the same folder so the gateway hands out that routine’s URL and key; a null key is retried, never replaced by a second routine. ensure_agent_webhook_routines does that without opening stores. MachineClient::poll_agent_directory retries and rescans for new bots. The URL and key live in memory and in the session’s keychain file under the store root, and are not logged. This path does not read LEADER_SOCK_ENV.

A node is OpenedStore::connect_node_from_env. One CLI session, woken by ACP on LEADER_SOCK_ENV. A routine URL or bearer in the environment is refused before register. This path does not gain a webhook config. OpenedStore::connect still registers one session a caller already built; it is not either of those open paths and it does not read a wake from the environment.

Re-exports§

pub use provider::plan_chain;
pub use provider::HostEnv;
pub use provider::HookFlavor;
pub use provider::ResumeSpawnAdapter;
pub use provider::SessionRecord;
pub use provider::WakeChain;
pub use provider::WebVendor;
pub use provider::adapter_for;
pub use provider::wake_prompt;
pub use provider::AdapterConfig;
pub use provider::ClaudeChannelAdapter;
pub use provider::ClaudeRoutineFireAdapter;
pub use provider::CodexAppServerAdapter;
pub use provider::CodexCloudAdapter;
pub use provider::CodexEndpoint;
pub use provider::CursorAgentAdapter;
pub use provider::GrokLeaderAdapter;
pub use provider::KimiServerAdapter;
pub use provider::NoInboundAdapter;
pub use provider::ProviderKind;
pub use provider::ProviderSession;
pub use provider::RoutineWebhookAdapter;
pub use provider::SessionKind;
pub use provider::Surface;
pub use provider::WakeAdapter;
pub use provider::WakeError;
pub use provider::WakeLetter;
pub use provider::WakeOutcome;
pub use provider::INBOX_DIR_ENV;
pub use provider::PROVIDER_ENV;

Modules§

cli
The client’s commands. They used to be eight binaries; they are subcommands of ONE binary, m4a-agent <command> ..., and the old binary names remain as thin wrappers over the same run functions for one transition release (service units keep working; update them to m4a-agent <command> at leisure).
provider
Provider wake adapters for the one-per-machine mail4agent clients.
resolve
The five places a session id used to come from, as sources of ONE resolver chain (m4a_agent::ResolverChain). Each source only names the session (and, when it knows it, the tier-1 local session id); identity, nick and credentials are the client’s identity store’s. The chain refuses two sources that name different sessions for one process.
store_key
The store key. It used to be SHA-256 of the session id: anyone who could guess the id and read the directory could open the store. Now it is 32 random bytes in the client’s vault (<store_root>/.m4a-agent/), and stores sealed under the old derivation are moved over, one way:
wake_policy
When a session’s wake fires, and what it last did. Two small non-secret files in the session’s store directory, written by m4a-agent wake and read by the running client at every wake:

Structs§

CmdReply
The answer to one CmdRequest.
CmdRequest
One command request.
DecryptedWake
One decrypted room text the routine should learn about.
DeviceId
A device id: an opaque, server-assigned string with no sigil, one per authenticated session (plan §3.1’s MessengerCore::device_id).
EventId
A Matrix event id: $opaque (room version 3+ shape — no server-name suffix; the opaque part is itself a content hash in most room versions, but this crate does not interpret it further).
FoundSession
A session found by nick. No routine URL and no bearer.
GrokListener
Push listener for every live local grok session.
Heard
One live grok CLI session the index named.
HostSession
One bot session the host says lives on this machine.
ListenReport
What one GrokListener::tick did. Nicks only. No bearer, no session id.
MachineClient
NodeClient
One local ACP node: a single OpenedStore, its push link, and an optional send socket for m4a-send.
NodeTickReport
What one NodeClient::tick did. Event ids, room ids, and error texts only — never URLs, keys, or bearers.
OpenedStore
A sealed messenger core opened for one session, plus the homeserver it performs released requests against. The Olm account was loaded or created into it. Private key material stays here. The device bearer stays in memory and is zeroed when this value is dropped.
OutgoingMessage
One message a caller wants sent via MessengerCommand::SendMessage — M13b’s binding content shape: text/notice/emote, an optional reply pointer, and an optional edit target (which must name one of the caller’s own already-sent events — MessengerCore::dispatch rejects an edit of someone else’s event rather than silently sending a replacement the room’s other members will refuse to accept, per m.replace’s own “must come from the original sender” rule, crate::room::relations’s own module doc). reply_to and edit_of are mutually exclusive at the wire level — if both are set, edit_of wins (an edit’s own m.relates_to is m.replace, not a reply).
PushedRoomEvent
One room event the homeserver pushed for a single session.
RoomId
A Matrix room id: !opaque:server_name (client-server API’s own room_id grammar — opaque per spec, never derived from the room’s own alias or name).
RoomView
One room this store currently holds, from this account’s own view.
RoutineReport
One bot’s wake routine, by nick and folder id.
RoutineWake
One URL per bot, already created. Neighbors are addressed by nick or mxid on the server.
SendReply
The answer to one SendRequest.
SendRequest
One request on the send socket.
SessionConfig
One session of the client, ready to log in. Built from the host environment (SessionConfig::from_env) or SessionConfig::new_identity. The nick is the one the operator assigned with the invite; it is known after the first login.
SessionWake
Wake targets for one shell. Empty fields mean that trigger is off. routine_bearer is kept only in memory and only attached when routine_url is set. The caller fills it from the environment, never from a literal in source.
TextView
One text-like timeline row. Ciphertext is not included.
TickReport
What one MachineClient::tick did. Nicks, event ids, room ids, and error texts only.
UserId
A Matrix user id: @localpart:server_name (client-server API’s own user_id grammar).
WakeAttempt
One routine POST: the Matrix event id it carried and the HTTP status (None when no response arrived). 200 means the routine woke.
WakeOptions
Knobs for ensure_agent_webhook_routines. All machine-specific, so they come from the environment (WakeOptions::from_lookup).

Enums§

CreateRoomKind
Which shape MessengerCommand::CreateRoom builds, mapped onto the server’s own POST /createRoom field set (visibility/is_direct/ invite/name/topic — the server derives kind itself from visibility+is_direct, per the server plan’s §5; a client never sends a preset/kind field directly).
MessageKind
Which m.room.message shape OutgoingMessage renders as (M13b send pipeline).
MessengerCommand
One command a shell/adapter dispatches into the core — both the M13a receive-side variants (MarkRead, SetTyping, RetryDecryption, LoadOlder) and the M13b send/room-membership ones. Only PartialEq (not Eq): MessengerCommand::SetTag’s order: Option<f64> cannot implement Eq.
RoomKind
What kind of room this is, derived from its current state – never a separately stored flag (see RoomState::derive_room_kind’s own doc for the precedence between the two possible signals).
ShellError
Why opening the client store, releasing a send, or posting a routine failed. The seal key and the device bearer are never included.
WakeStatus
Where one bot’s wake stands after ensure_agent_webhook_routines. Carries no URL and no key.

Constants§

AGENTS_DIR_ENV
Live Grok Bot agents on this machine. Each child folder name is an agent id; profile.json has the display name. MachineClient::from_env prefers this over SESSIONS_DIR_ENV when the directory exists.
AGENT_RESCAN_SECS_ENV
BOT_NAME_ENV
Display name of this Grok Bot web session, the name the host already shows. The nick is derived from it. This is not a nick slug.
CONFIG_ENV
Optional path of a toml file whose homeserver_url is used when HOMESERVER_URL_ENV is unset. Unset looks at ./mail4agent.toml and ignores it when that file is absent.
DEFAULT_AGENTS_DIR
Default agents directory, relative to the user’s home ($HOME or %USERPROFILE%). The webhook command uses it when AGENTS_DIR_ENV is unset and this path is a directory. The web client does not.
DEFAULT_SOCK_NAME
Socket file name under the store root when SEND_SOCK_ENV is unset.
ENV_FILE_ENV
Env file with the machine’s client settings (KEY=VALUE lines), read by m4a-web-client and m4a-send for every variable the process environment leaves unset. Default $HOME/.config/mail4agent/web-client.env. Settings only: homeserver URL, store root, keychain dir, skip list, session-id aliases. Never secrets.
HOMESERVER_URL_ENV
Homeserver origin for a Grok Bot web session, for example http://127.0.0.1:8741. Wins over CONFIG_ENV / mail4agent.toml. There is no built-in host.
LEADER_CWD_ENV
session/load working directory when LEADER_SOCK_ENV is set. Unset uses the process current directory.
LEADER_SOCK_ENV
Leader socket for a local session (leader.sock). Unset: no ACP push.
LOCAL_BUS_ENV
Optional rescan period in seconds for MachineClient::poll_agent_directory. Unset or 0 means the caller decides when to poll; open still scans once. 1/on: the sessions this client holds reach each other through the in-process bus and the homeserver is not called after opening (registration at open still uses it). A peer that is not a session of this client is then unreachable; leave it off for ordinary operation.
MAX_SEND_BYTES
Longest text one send carries.
NODE_DEFAULT_SOCK_NAME
Default send-socket file name under the store root for the node client.
PRODUCT_INVITE_ENV
Identity mode: the operator’s one-time invite code (read by the CLIENT process, once; the agent is never given it). Not needed after the identity is enrolled.
PRODUCT_URL_ENV
Product-session mode: base URL of the product server. Every Client-Server call goes through its proxy with the product session token as bearer.
PROFILE_NOTE_ENV
Set to 1 to write the bootstrap note into the profile description of each bot that has no routine yet, through the gateway’s updateAgent (the host pushes profile edits to the server copy of the bot). The note asks the bot to create its own routine named crate::routine_folder_id of its nick on its next turn. Off by default: it edits a description the owner wrote.
ROUTINE_BEARER_ENV
Optional Authorization: Bearer for ROUTINE_URL_ENV. Sent only when that URL is also set. A host keychain injects this into the process environment from outside. It is never written to disk and never read from source.
ROUTINE_URL_ENV
Routine POST target. Unset or empty: no POST. There is no default URL.
SEND_COMMAND
Command a woken bot runs to answer, as shown in the wake’s reply.
SEND_SOCK_ENV
Path of the web client’s local send socket.
SESSIONS_DIR_ENV
Directory of session records. Each *.json file is bot_name, session_id, and an optional agent_id. Used when no agents directory is available. Unset means the host passed the list to MachineClient::open instead.
SESSION_IDS_ENV
Comma-separated agent_id=session_id pairs. An agent listed here uses that mail session id instead of its agent id, for a bot whose session was registered before agent-id sessions. Machine-specific, so it lives in the environment.
SESSION_ID_ENV
Session id the web host already assigned. The store directory is session_store_dir of this id. The same id reopens the same store.
SKIP_NICKS_ENV
Comma-separated nicks this client leaves alone: no session, no mirror, no credential call. Machine-specific, so it lives in the environment and not in source.
STORE_LOCK_FILE
Lock file in each sealed session directory. MachineClient::open holds an exclusive lock on it for as long as the client lives, so a second process (another client, or m4a-send opening the store itself) cannot write the same Olm state at the same time.
STORE_ROOT_ENV
Shared store root on this machine. The caller supplies it. Unset is not a shared fallback outside tests.
TIER_ENV
Identity mode: server (our product API, default) or matrix (the Matrix client API).
WAKE_KEYCHAIN_FILE
Per-session keychain file under the session’s sealed directory (crate::session_store_dir). Mode 0600. Holds the folder id, the webhook URL, and its key. Outside any repository; never logged.

Functions§

ensure_agent_webhook_routines
Ensures each agent’s local mirror (webhook trigger, disabled, folder = crate::routine_folder_id of the nick) and asks the gateway for its credential. Bots whose nick is in skip_nicks are not touched. Does not open sealed stores, does not register on the homeserver, and does not log or return URLs or keys. When store_root is set, a ready URL and key are written to that session’s keychain file. With profile_note, a bot still waiting gets the bootstrap note.
ensure_agent_webhook_routines_from_env
ensure_agent_webhook_routines using the host gateway file, the agents directory from the environment (or DEFAULT_AGENTS_DIR), SKIP_NICKS_ENV, and STORE_ROOT_ENV when set.
hear
Reads an active_sessions.json document and names each row from that session’s own topic. Empty text is an empty list. A document that is not that index is an error.
load_agents_dir
Reads each agent folder under dir. Folder name is the agent id and the mail session id. profile.json supplies the display name. Folders without a usable profile are skipped. This does not hardcode agent ids.
load_env_file
Sets each KEY=VALUE from the env file (ENV_FILE_ENV or the default web-client path) that the environment does not already have. Blank lines and # comments are skipped; surrounding quotes are dropped. Call at the start of main, before any thread starts. Returns the file read.
load_env_file_named
Like load_env_file, but the default under <home>/.config/mail4agent/ is default_name (node uses node-client.env). Home is HOME when non-empty, otherwise USERPROFILE. ENV_FILE_ENV still wins when set.
load_session_records
Reads *.json session records from dir. A file that carries anything besides bot_name, session_id, and agent_id is refused, so a webhook URL or a bearer cannot ride along in a world-readable record.
mxid_localpart
Localpart of a Matrix user id (@alice:server -> alice).
nick_from_display_name
Cyrillic (and a few adjacent letters) to Latin, then the host’s routine slug rule, so a bot’s nick, its wake routine name, and that routine’s folder id are one string.
post_decrypted
POSTs wake once, as a JSON object, to url. No mailbox path and no Authorization header. url is the bot’s already-configured routine. http and https are both followed; anything else is refused before a socket is opened.
post_decrypted_with_bearer
post_decrypted plus the routine key. When bearer is Some and non-empty, that same value is sent as Authorization: Bearer and as X-Automation-Key. Content-Type is application/json. One JSON body. One attempt, 8 seconds. A 200 means the routine woke. Anything else is a failure and is not retried here. The key and the URL are not included in the error. The value must come from memory, not from source.
post_routine_json
post_decrypted_with_bearer with any JSON object as the body: the machine client’s own events (kind = peer_joined) go to a bot’s routine this way. Same headers, one attempt, 8 seconds, 200 or error. The key and the URL are not included in the error.
reply_hint
The reply hint for a wake from from_nick to to.
routine_folder_id
Folder id the Grok Bot routine store gives a routine named name.
send_cmd_via_socket
Sends one rich command over the client’s socket and waits for the answer.
send_sock_path
Socket path: SEND_SOCK_ENV when set, else DEFAULT_SOCK_NAME under store_root.
send_sock_path_named
Like send_sock_path, but uses default_name under store_root when SEND_SOCK_ENV is unset (node client uses node-client.sock).
send_via_socket
Sends request over the client’s socket and waits up to wait for the answer. Err means no client answered on that socket (missing, refused, or closed early), so the caller may fall back to opening the session itself.
session_store_dir
Directory for session_id under root.
store_root
STORE_ROOT_ENV when set and non-empty. Tests with it unset get one temp directory for this process. Other builds return ShellError::StoreRoot and do not invent a path two agents would share.
store_seal_key
SHA-256 of session_id’s UTF-8 bytes. That digest is the check that this session may open the store. The bytes are not written to disk.