Documentation
¶
Index ¶
- Constants
- Variables
- func IsValidChannelType(t string) bool
- func QRHalfBlocks(payload string) (string, error)
- func QRPNGDataURI(payload string) (string, error)
- func ShouldForwardStepToSlack(step *sharedmsg.MsgAgenticStep) bool
- func SlackStepLine(step *sharedmsg.MsgAgenticStep) string
- func ToSlackMrkdwn(text string) string
- type Attachment
- type ChannelAdapter
- type ChatbotAdapter
- func (c *ChatbotAdapter) InboundCh() <-chan InboundMessage
- func (c *ChatbotAdapter) Send(_ context.Context, outbound OutboundMessage) error
- func (c *ChatbotAdapter) SetReplyMode(_ string)
- func (c *ChatbotAdapter) Start(ctx context.Context) error
- func (c *ChatbotAdapter) Status() msg.ChannelStatus
- func (c *ChatbotAdapter) Stop() error
- func (c *ChatbotAdapter) Type() string
- type CredStore
- type DiscordAdapter
- func (d *DiscordAdapter) InboundCh() <-chan InboundMessage
- func (d *DiscordAdapter) Send(_ context.Context, outbound OutboundMessage) error
- func (d *DiscordAdapter) SetReplyMode(mode string)
- func (d *DiscordAdapter) Start(ctx context.Context) error
- func (d *DiscordAdapter) Status() msg.ChannelStatus
- func (d *DiscordAdapter) Stop() error
- func (d *DiscordAdapter) Type() string
- type Draft
- type DraftStore
- func (ds *DraftStore) AddAttachment(id string, att Attachment) error
- func (ds *DraftStore) Approve(id string) bool
- func (ds *DraftStore) ApproveLatest() (string, bool)
- func (ds *DraftStore) Create(to, subject, body, inReplyTo string) string
- func (ds *DraftStore) Delete(id string)
- func (ds *DraftStore) Get(id string) (*Draft, bool)
- func (ds *DraftStore) List() []*Draft
- func (ds *DraftStore) Update(id, body string) error
- type EmailAdapter
- func (e *EmailAdapter) InboundCh() <-chan InboundMessage
- func (e *EmailAdapter) ListEmails(count int, search string) ([]msg.EmailSummary, error)
- func (e *EmailAdapter) ReadEmail(uid int) (*msg.EmailDetail, error)
- func (e *EmailAdapter) ResolveShortID(id string) (int, bool)
- func (e *EmailAdapter) Send(_ context.Context, outbound OutboundMessage) error
- func (e *EmailAdapter) SendEmail(to, subject, body, inReplyTo string, attachments []Attachment) error
- func (e *EmailAdapter) SetReplyMode(_ string)
- func (e *EmailAdapter) Start(ctx context.Context) error
- func (e *EmailAdapter) Status() msg.ChannelStatus
- func (e *EmailAdapter) Stop() error
- func (e *EmailAdapter) Type() string
- type IMessageAdapter
- func (i *IMessageAdapter) InboundCh() <-chan InboundMessage
- func (i *IMessageAdapter) Send(ctx context.Context, outbound OutboundMessage) error
- func (i *IMessageAdapter) SetReplyMode(mode string)
- func (i *IMessageAdapter) Start(ctx context.Context) error
- func (i *IMessageAdapter) Status() msg.ChannelStatus
- func (i *IMessageAdapter) Stop() error
- func (i *IMessageAdapter) Type() string
- type InboundMessage
- type LinkableChannel
- type OutboundMessage
- type PairingChannel
- type PairingEvent
- type PairingOption
- type PairingRecord
- type PairingStore
- func (s *PairingStore) AddPending(humanoid, channel string, im InboundMessage) (PendingReq, error)
- func (s *PairingStore) Allow(humanoid, channel, sender string) error
- func (s *PairingStore) Allowed(humanoid, channel, sender string) bool
- func (s *PairingStore) Approve(humanoid, channel, code string) (PendingReq, bool, error)
- func (s *PairingStore) List(humanoid, channel string) (PairingRecord, error)
- func (s *PairingStore) LoadDeviceLink(humanoid, channel string) (json.RawMessage, bool)
- func (s *PairingStore) SaveDeviceLink(humanoid, channel string, blob json.RawMessage) error
- type PendingReq
- type PhoneAdapter
- func (p *PhoneAdapter) InboundCh() <-chan InboundMessage
- func (p *PhoneAdapter) RecentMessages(count int) []PhoneMessage
- func (p *PhoneAdapter) Send(ctx context.Context, outbound OutboundMessage) error
- func (p *PhoneAdapter) SetReplyMode(_ string)
- func (p *PhoneAdapter) Start(ctx context.Context) error
- func (p *PhoneAdapter) Status() msg.ChannelStatus
- func (p *PhoneAdapter) Stop() error
- func (p *PhoneAdapter) Type() string
- func (p *PhoneAdapter) Validate(ctx context.Context) error
- type PhoneMessage
- type SignalAdapter
- func (s *SignalAdapter) InboundCh() <-chan InboundMessage
- func (s *SignalAdapter) PairingCh() <-chan PairingEvent
- func (s *SignalAdapter) Send(ctx context.Context, outbound OutboundMessage) error
- func (s *SignalAdapter) SetReplyMode(mode string)
- func (s *SignalAdapter) Start(ctx context.Context) error
- func (s *SignalAdapter) Status() msg.ChannelStatus
- func (s *SignalAdapter) Stop() error
- func (s *SignalAdapter) TriggerLink(force bool) error
- func (s *SignalAdapter) Type() string
- type SlackAdapter
- func (s *SlackAdapter) GetMessage(id string) (SlackMessage, bool)
- func (s *SlackAdapter) InboundCh() <-chan InboundMessage
- func (s *SlackAdapter) RecentMessages(count int) []SlackMessage
- func (s *SlackAdapter) Send(_ context.Context, outbound OutboundMessage) error
- func (s *SlackAdapter) SetReplyMode(mode string)
- func (s *SlackAdapter) Start(ctx context.Context) error
- func (s *SlackAdapter) Status() msg.ChannelStatus
- func (s *SlackAdapter) Stop() error
- func (s *SlackAdapter) Type() string
- func (s *SlackAdapter) Validate(ctx context.Context) error
- type SlackMessage
- type TeamsAdapter
- func (t *TeamsAdapter) InboundCh() <-chan InboundMessage
- func (t *TeamsAdapter) Send(ctx context.Context, outbound OutboundMessage) error
- func (t *TeamsAdapter) SetReplyMode(mode string)
- func (t *TeamsAdapter) Start(ctx context.Context) error
- func (t *TeamsAdapter) Status() msg.ChannelStatus
- func (t *TeamsAdapter) Stop() error
- func (t *TeamsAdapter) Type() string
- func (t *TeamsAdapter) Validate(ctx context.Context) error
- type TelegramAdapter
- func (t *TelegramAdapter) InboundCh() <-chan InboundMessage
- func (t *TelegramAdapter) Send(ctx context.Context, outbound OutboundMessage) error
- func (t *TelegramAdapter) SetReplyMode(mode string)
- func (t *TelegramAdapter) Start(ctx context.Context) error
- func (t *TelegramAdapter) Status() msg.ChannelStatus
- func (t *TelegramAdapter) Stop() error
- func (t *TelegramAdapter) Type() string
- type Validator
- type WhatsAppAdapter
- func (w *WhatsAppAdapter) GetMessage(id string) (WhatsAppMessage, bool)
- func (w *WhatsAppAdapter) InboundCh() <-chan InboundMessage
- func (w *WhatsAppAdapter) RecentMessages(count int) []WhatsAppMessage
- func (w *WhatsAppAdapter) RelayMode() bool
- func (w *WhatsAppAdapter) Send(ctx context.Context, outbound OutboundMessage) error
- func (w *WhatsAppAdapter) SendTemplate(ctx context.Context, to, name, lang string, bodyParams []string) error
- func (w *WhatsAppAdapter) SetConfig(cfg msg.ChannelConfig)
- func (w *WhatsAppAdapter) SetReplyMode(_ string)
- func (w *WhatsAppAdapter) Start(ctx context.Context) error
- func (w *WhatsAppAdapter) Status() msg.ChannelStatus
- func (w *WhatsAppAdapter) Stop() error
- func (w *WhatsAppAdapter) Type() string
- func (w *WhatsAppAdapter) Validate(ctx context.Context) error
- type WhatsAppMessage
Constants ¶
const ( // OutboundKindMessage is a normal message (the default zero value). OutboundKindMessage = "" // OutboundKindStep is a compact progress step title from an in-flight // agentic session. Channels with rich formatting render it de-emphasized // (Slack: a context block in the reply thread) so the user sees the // session progressing without the full transcript that the pane // terminal shows. OutboundKindStep = "step" )
OutboundKind values classify an outbound message for channel-specific rendering.
Variables ¶
var PluginLookup func(channelType string, config msg.ChannelConfig) (ChannelAdapter, bool)
PluginLookup, when non-nil, resolves a non-built-in channel type to an out-of-process plugin adapter (internal/channels/plugin, design 002). It is a nil-safe hook set by plugin.Wire from the CLI layer so this package never imports the plugin package (no import cycle). Built-ins always win: the factory consults this only after its built-in switch, so a plugin named "slack" can never shadow the built-in adapter.
var PluginTypeKnown func(channelType string) bool
PluginTypeKnown, when non-nil, reports whether an installed channel plugin provides the given type (companion hook to PluginLookup; same wiring).
var ValidChannelTypes = []string{"whatsapp", "slack", "email", "phone", "chatbot", "discord", "telegram", "signal", "imessage", "teams"}
ValidChannelTypes lists the BUILT-IN channel type strings (installed plugin channels are additionally accepted via the PluginTypeKnown hook).
Functions ¶
func IsValidChannelType ¶
IsValidChannelType checks if a channel type string is supported: a built-in type, or an installed plugin channel via the nil-safe hook.
func QRHalfBlocks ¶
QRHalfBlocks renders payload as a scannable QR using Unicode half-block runes — two vertical modules per text row, so modules come out roughly square in a terminal cell. It is tuned for a DARK background: light modules (including the quiet zone) are drawn as block "ink" and dark modules as spaces, which on a dark terminal reads as a normal dark-on-light code that phone cameras scan. On a light-background terminal the code appears inverted; QRPNGDataURI and the raw payload are the theme-independent fallbacks.
func QRPNGDataURI ¶
QRPNGDataURI renders payload as a black-on-white PNG QR and returns it as a "data:image/png;base64,…" URI an <img> can show directly. This is the theme-independent, guaranteed-scannable form carried to the web dashboard.
func ShouldForwardStepToSlack ¶
func ShouldForwardStepToSlack(step *sharedmsg.MsgAgenticStep) bool
ShouldForwardStepToSlack reports whether a step event earns a progress line in the Slack flow.
func SlackStepLine ¶
func SlackStepLine(step *sharedmsg.MsgAgenticStep) string
SlackStepLine renders one agentic step event as a single compact mrkdwn line for the Slack progress flow: an emoji, optional nesting marker for sub-agent depth, and the step TITLE only (never the step detail/payload).
func ToSlackMrkdwn ¶
ToSlackMrkdwn converts the session's markdown-ish LLM output to Slack mrkdwn so the final answer reads natively in Slack:
**bold** → *bold* # Heading → *Heading* - bullet → • bullet [text](url) → <url|text> ```lang fences → ``` (Slack fences carry no language tag)
Inline code, plain *italic*, and everything inside code fences are left as-is (Slack renders backtick spans natively).
Types ¶
type Attachment ¶
Attachment holds a file attachment for an email draft.
type ChannelAdapter ¶
type ChannelAdapter interface {
// Type returns the channel type string (e.g., "whatsapp", "slack", "email", "phone", "chatbot").
Type() string
// Start connects to the external service and begins listening for inbound messages.
// The provided context is cancelled when the adapter should stop.
Start(ctx context.Context) error
// Stop gracefully disconnects from the external service.
Stop() error
// Send delivers a message back to the external channel.
Send(ctx context.Context, outbound OutboundMessage) error
// InboundCh returns a channel that emits inbound messages from the external platform.
InboundCh() <-chan InboundMessage
// Status returns the current connection status.
Status() msg.ChannelStatus
// SetReplyMode changes the reply mode at runtime.
// Mode is "messages" (respond to all channel messages) or "mentions"
// (respond only when the bot is @mentioned).
SetReplyMode(mode string)
}
ChannelAdapter is the interface all external communication channel adapters must implement. Each adapter bridges one external platform (WhatsApp, Slack, Email, Phone, or Chatbot) with the humanoid actor system.
Lifecycle: Start() is called when the humanoid is spawned with a configured channel. Stop() is called on humanoid shutdown or explicit channel stop. Inbound messages arrive via the InboundCh() channel and are forwarded to the humanoid's LLMPromptExecutionActor as prompts. Outbound responses are delivered via Send().
func NewAdapter ¶
func NewAdapter(channelType string, config msg.ChannelConfig) (ChannelAdapter, error)
NewAdapter creates a ChannelAdapter for the given channel type and configuration. Returns an error if the channel type is not supported.
type ChatbotAdapter ¶
type ChatbotAdapter struct {
// contains filtered or unexported fields
}
ChatbotAdapter implements the ChannelAdapter interface for connecting to server-side chatbot panes via the rysh-server REST API. It monitors active chatbot sessions and forwards visitor messages to the humanoid's inbound channel, enabling observation and human takeover.
X2 fix. Two defects were fixed together here, because either alone left the channel silently dead:
- Every URL was wrong. The adapter called /api/chatbot/operator/sessions and friends, which do not exist on rysh-server — the operator routes are nested under /api/workspaces/:wsID/chatbots/:id/sessions/... So every request 404'd, refreshSessions swallowed the error at Debug level, and Start still reported Connected:true.
- Nothing ever pushed to c.inbound. The channel was allocated and returned by InboundCh, but no code path ever sent on it, so a visitor message could never reach the humanoid even had the URLs been right.
Inbound now polls GetMessages per tracked session using its after_seq cursor, so each visitor message is delivered exactly once.
func NewChatbotAdapter ¶
func NewChatbotAdapter(config msg.ChannelConfig) *ChatbotAdapter
NewChatbotAdapter creates a new Chatbot adapter for remote server mode.
func (*ChatbotAdapter) InboundCh ¶
func (c *ChatbotAdapter) InboundCh() <-chan InboundMessage
InboundCh returns the channel for receiving inbound chatbot messages.
func (*ChatbotAdapter) Send ¶
func (c *ChatbotAdapter) Send(_ context.Context, outbound OutboundMessage) error
Send delivers a human operator message to a chatbot session on the server. This triggers takeover if not already taken over.
func (*ChatbotAdapter) SetReplyMode ¶
func (c *ChatbotAdapter) SetReplyMode(_ string)
SetReplyMode is a no-op for chatbot.
func (*ChatbotAdapter) Start ¶
func (c *ChatbotAdapter) Start(ctx context.Context) error
Start connects to the remote server and begins monitoring chatbot sessions.
func (*ChatbotAdapter) Status ¶
func (c *ChatbotAdapter) Status() msg.ChannelStatus
Status returns the current adapter status.
func (*ChatbotAdapter) Stop ¶
func (c *ChatbotAdapter) Stop() error
Stop gracefully disconnects from the server.
type CredStore ¶
type CredStore interface {
LoadDeviceLink(humanoid, channel string) (json.RawMessage, bool)
SaveDeviceLink(humanoid, channel string, blob json.RawMessage) error
}
CredStore is the two-method view of PairingStore handed to QR adapters at construction so they can persist/resume their own device-link session without the actor (or the skill file) ever holding the credentials.
type DiscordAdapter ¶
type DiscordAdapter struct {
// contains filtered or unexported fields
}
DiscordAdapter bridges Discord (bot Gateway) to the humanoid actor system.
func NewDiscordAdapter ¶
func NewDiscordAdapter(config msg.ChannelConfig) *DiscordAdapter
NewDiscordAdapter creates a Discord channel adapter from the config.
func (*DiscordAdapter) InboundCh ¶
func (d *DiscordAdapter) InboundCh() <-chan InboundMessage
InboundCh returns the inbound message channel.
func (*DiscordAdapter) Send ¶
func (d *DiscordAdapter) Send(_ context.Context, outbound OutboundMessage) error
Send delivers an outbound message to a Discord channel. The recipient is the channel (or thread) ID; when RecipientID is empty it falls back to ThreadID, which HumanoidActor sets from the inbound message.
func (*DiscordAdapter) SetReplyMode ¶
func (d *DiscordAdapter) SetReplyMode(mode string)
SetReplyMode changes the reply mode ("messages" or "mentions") at runtime.
func (*DiscordAdapter) Start ¶
func (d *DiscordAdapter) Start(ctx context.Context) error
Start connects to the Discord Gateway and begins listening for messages.
Unlike Slack's socketmode (where RunContext exits on disconnect and we run our own reconnect loop), discordgo owns its read/heartbeat goroutines and reconnects with backoff internally (ShouldReconnectOnError defaults to true). We therefore do not duplicate the Slack connectionLoop; instead we register Connect/Disconnect/Resumed handlers so Status() still reflects explicit connection-state transitions across those internal reconnects.
func (*DiscordAdapter) Status ¶
func (d *DiscordAdapter) Status() msg.ChannelStatus
Status reports the connection status.
func (*DiscordAdapter) Stop ¶
func (d *DiscordAdapter) Stop() error
Stop gracefully disconnects from Discord. Safe to call before Start or more than once.
type Draft ¶
type Draft struct {
ID string
To string
Subject string
Body string
InReplyTo string
Attachments []Attachment
CreatedAt time.Time
// ApprovedAt is set when the OWNER confirms the draft (typing "send" in
// the humanoid's pane). Zero means pending.
//
// This is the enforcement state behind human-governed channels. Without it
// the "draft-and-confirm" guarantee is only prompt text — the model is
// told not to send unbidden, and nothing stops it if it does. A send tool
// under human governance must refuse a draft whose ApprovedAt is zero.
ApprovedAt time.Time
}
Draft represents an unsent draft.
type DraftStore ¶
type DraftStore struct {
// contains filtered or unexported fields
}
DraftStore provides thread-safe in-memory draft storage.
func (*DraftStore) AddAttachment ¶
func (ds *DraftStore) AddAttachment(id string, att Attachment) error
AddAttachment appends an attachment to an existing draft.
func (*DraftStore) Approve ¶
func (ds *DraftStore) Approve(id string) bool
Approve marks a draft as owner-confirmed. Returns false if the ID is unknown.
func (*DraftStore) ApproveLatest ¶
func (ds *DraftStore) ApproveLatest() (string, bool)
ApproveLatest confirms the most recently created pending draft and returns its ID.
This backs the bare "send" confirmation, where the owner names no ID: the draft they are looking at is the one just created. Only PENDING drafts are considered, so repeating "send" cannot silently re-approve an older draft that was already sent and deleted, nor resurrect one the owner ignored.
func (*DraftStore) Create ¶
func (ds *DraftStore) Create(to, subject, body, inReplyTo string) string
Create adds a new draft and returns its ID.
func (*DraftStore) Get ¶
func (ds *DraftStore) Get(id string) (*Draft, bool)
Get returns a draft by ID.
func (*DraftStore) Update ¶
func (ds *DraftStore) Update(id, body string) error
Update replaces the body of an existing draft.
type EmailAdapter ¶
type EmailAdapter struct {
// contains filtered or unexported fields
}
EmailAdapter implements the ChannelAdapter interface for Email using IMAP/SMTP. It monitors an inbox via IMAP IDLE for real-time message arrival and replies via SMTP.
func NewEmailAdapter ¶
func NewEmailAdapter(config msg.ChannelConfig) *EmailAdapter
NewEmailAdapter creates a new Email adapter with the given configuration.
func (*EmailAdapter) InboundCh ¶
func (e *EmailAdapter) InboundCh() <-chan InboundMessage
InboundCh returns the channel for receiving inbound emails.
func (*EmailAdapter) ListEmails ¶
func (e *EmailAdapter) ListEmails(count int, search string) ([]msg.EmailSummary, error)
ListEmails opens a short-lived IMAP session and returns summaries of recent emails.
func (*EmailAdapter) ReadEmail ¶
func (e *EmailAdapter) ReadEmail(uid int) (*msg.EmailDetail, error)
ReadEmail opens a short-lived IMAP session and fetches a single email by UID.
func (*EmailAdapter) ResolveShortID ¶
func (e *EmailAdapter) ResolveShortID(id string) (int, bool)
ResolveShortID maps a short handle ("a3f9") back to an email UID. Matching is case-insensitive. Returns false if the handle is unknown (e.g. listed in a different session).
func (*EmailAdapter) Send ¶
func (e *EmailAdapter) Send(_ context.Context, outbound OutboundMessage) error
Send delivers a reply email via SMTP.
func (*EmailAdapter) SendEmail ¶
func (e *EmailAdapter) SendEmail(to, subject, body, inReplyTo string, attachments []Attachment) error
SendEmail sends an email with optional attachments. This is the full-featured send method used by the email_send tool (as opposed to Send() which is the simple ChannelAdapter interface method for auto-replies).
func (*EmailAdapter) SetReplyMode ¶
func (e *EmailAdapter) SetReplyMode(_ string)
SetReplyMode is a no-op for email (all inbound emails are processed).
func (*EmailAdapter) Start ¶
func (e *EmailAdapter) Start(ctx context.Context) error
Start connects to the IMAP server and begins monitoring the inbox via IDLE.
func (*EmailAdapter) Status ¶
func (e *EmailAdapter) Status() msg.ChannelStatus
Status returns the current Email connection status.
func (*EmailAdapter) Stop ¶
func (e *EmailAdapter) Stop() error
Stop gracefully disconnects from the IMAP server.
type IMessageAdapter ¶
type IMessageAdapter struct {
// contains filtered or unexported fields
}
IMessageAdapter bridges iMessage (macOS Messages.app) to the humanoid actor system: AppleScript for outbound, a read-only chat.db poll for inbound.
func NewIMessageAdapter ¶
func NewIMessageAdapter(config msg.ChannelConfig) *IMessageAdapter
NewIMessageAdapter creates an iMessage channel adapter from the config.
func (*IMessageAdapter) InboundCh ¶
func (i *IMessageAdapter) InboundCh() <-chan InboundMessage
InboundCh returns the inbound message channel.
func (*IMessageAdapter) Send ¶
func (i *IMessageAdapter) Send(ctx context.Context, outbound OutboundMessage) error
Send delivers an outbound message via AppleScript (osascript). Sends are serialized and paced (~1 msg/s). The message body and recipient are passed as `on run argv` arguments — never interpolated into the AppleScript source — so user content cannot inject script.
func (*IMessageAdapter) SetReplyMode ¶
func (i *IMessageAdapter) SetReplyMode(mode string)
SetReplyMode stores the mode but is effectively a no-op: iMessage is a DM-style channel and all allowed inbound is processed (design §4.5).
func (*IMessageAdapter) Start ¶
func (i *IMessageAdapter) Start(ctx context.Context) error
Start verifies the macOS host bridge (darwin, chat.db readable, sqlite3 on PATH), snapshots the current MAX(message.ROWID) so only NEW messages flow, and launches the poll goroutine.
func (*IMessageAdapter) Status ¶
func (i *IMessageAdapter) Status() msg.ChannelStatus
Status reports the connection status. Connected flips true after the first successful poll; poll errors surface in Error.
type InboundMessage ¶
type InboundMessage struct {
SenderID string `json:"sender_id"`
SenderName string `json:"sender_name"`
Content string `json:"content"`
ThreadID string `json:"thread_id"`
Metadata map[string]string `json:"metadata,omitempty"`
}
InboundMessage represents a message received from an external channel. The json tags are the wire encoding of the channel-plugin protocol (internal/channels/plugin, design 002 §4.1).
type LinkableChannel ¶
LinkableChannel is implemented by PairingChannel adapters whose link flow can also be started on demand (`##humanoid pair link`, X4 design 009 §3.4) rather than only at Start. TriggerLink returns immediately: the flow runs on its own goroutine and reports through PairingCh. Unless force is set, implementations must refuse when the daemon already holds a linked account (the re-link guard), reporting the refusal as an "error" pairing event.
type OutboundMessage ¶
type OutboundMessage struct {
RecipientID string `json:"recipient_id"`
Content string `json:"content"`
ThreadID string `json:"thread_id"`
// Kind selects channel-specific rendering: OutboundKindMessage (default)
// or OutboundKindStep (compact progress title).
Kind string `json:"kind,omitempty"`
}
OutboundMessage represents a message to be sent to an external channel. The json tags are the wire encoding of the channel-plugin protocol (internal/channels/plugin, design 002 §4.1).
type PairingChannel ¶
type PairingChannel interface {
// PairingCh emits link-lifecycle events while Start() establishes the link.
PairingCh() <-chan PairingEvent
}
PairingChannel is implemented only by adapters that establish a session via an out-of-band device link (WhatsApp non-Cloud, Signal). Credential- configured adapters (Slack, WhatsApp Cloud, email) do NOT implement it.
type PairingEvent ¶
type PairingEvent struct {
Kind string // "qr" | "linked" | "error"
QR string // raw QR payload to render (Kind=="qr")
Session json.RawMessage // device-link creds to persist (Kind=="linked"; secret — never log)
Detail string // human message (Kind=="error")
}
PairingEvent is one device-link lifecycle event from a PairingChannel.
type PairingOption ¶
type PairingOption func(*PairingStore)
PairingOption customises a PairingStore at construction.
func WithMaxPending ¶
func WithMaxPending(n int) PairingOption
WithMaxPending overrides the pending-request cap (default 3).
func WithNow ¶
func WithNow(now func() time.Time) PairingOption
WithNow overrides the store's time source. Test seam: TTL expiry tests advance a fake clock instead of racing real sleeps against wall-clock TTLs (which flaked under -race on loaded CI runners).
func WithPendingTTL ¶
func WithPendingTTL(d time.Duration) PairingOption
WithPendingTTL overrides the pending-request lifetime (default 1h).
type PairingRecord ¶
type PairingRecord struct {
Allowlist []string `json:"allowlist"` // approved SenderIDs
Pending map[string]PendingReq `json:"pending"` // code -> request
DeviceLink json.RawMessage `json:"device_link"` // opaque adapter session creds (secret — never log)
UpdatedAt time.Time `json:"updated_at"`
}
PairingRecord is one (humanoid, channel) pairing state cell.
type PairingStore ¶
type PairingStore struct {
// contains filtered or unexported fields
}
PairingStore persists allowlists, pending pairing requests, and device-link creds. It lives in the channels package (not the actor) because it is shared between the actor's Receive goroutine and adapter/pairingLoop goroutines, so — per the project convention that only adapters may hold locks — it guards its read-modify-write cycles with a mutex here rather than in actor state.
func NewPairingStore ¶
func NewPairingStore(nc *nats.Conn, opts ...PairingOption) *PairingStore
NewPairingStore opens (or creates) the durable "rysh-pairings" KV bucket on nc's JetStream context, mirroring UsageActor.openKV. A nil connection or an unavailable JetStream leaves kv nil and every operation degrades to the file-backed channel-state path — the store never fails to construct.
func (*PairingStore) AddPending ¶
func (s *PairingStore) AddPending(humanoid, channel string, im InboundMessage) (PendingReq, error)
AddPending registers a pairing request for an unknown sender and returns the request (with its approval code). Expired entries are swept first; a sender that already has a live pending request gets the SAME request back (so repeated messages do not mint new codes or eat pending slots). When the post-sweep count is at maxPending, the request is refused with an error the caller can surface as a rate-note.
func (*PairingStore) Allow ¶
func (s *PairingStore) Allow(humanoid, channel, sender string) error
Allow adds sender directly to the allowlist, skipping the code flow. Additive and idempotent — used both by the ##humanoid allow command and by the spawn-time merge of a channel's declared allowlist.
func (*PairingStore) Allowed ¶
func (s *PairingStore) Allowed(humanoid, channel, sender string) bool
Allowed reports whether sender is on the (humanoid, channel) allowlist. An empty sender, a missing record, or unavailable storage all yield false — the admission gate is fail-closed (design 003 G5).
func (*PairingStore) Approve ¶
func (s *PairingStore) Approve(humanoid, channel, code string) (PendingReq, bool, error)
Approve consumes a pending code: the sender is promoted to the allowlist and the code is removed (single-use). An unknown or expired code is a no-op returning ok=false with no error, so the caller can print a notice.
func (*PairingStore) List ¶
func (s *PairingStore) List(humanoid, channel string) (PairingRecord, error)
List returns the current record for approver display, sweeping expired pendings first. The DeviceLink blob is blanked in the returned copy: it is a secret and List feeds rendering paths (pane / dashboard) that must never see it — use LoadDeviceLink for the credential itself.
func (*PairingStore) LoadDeviceLink ¶
func (s *PairingStore) LoadDeviceLink(humanoid, channel string) (json.RawMessage, bool)
LoadDeviceLink returns the persisted device-link blob, if any.
func (*PairingStore) SaveDeviceLink ¶
func (s *PairingStore) SaveDeviceLink(humanoid, channel string, blob json.RawMessage) error
SaveDeviceLink persists an adapter's opaque device-link session blob. The blob is a secret: it is stored (0600 file / FileStorage KV under .rysh/) and never logged or rendered.
type PendingReq ¶
type PendingReq struct {
Code string `json:"code"` // base36, from newPairCode()
SenderID string `json:"sender_id"`
SenderName string `json:"sender_name"`
Channel string `json:"channel"`
FirstMsg string `json:"first_msg"` // truncated, for the approver's context
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"` // CreatedAt + pending_ttl (default 1h)
}
PendingReq is one not-yet-approved contact request, keyed by its code.
type PhoneAdapter ¶
type PhoneAdapter struct {
// contains filtered or unexported fields
}
PhoneAdapter implements ChannelAdapter for Twilio SMS.
func NewPhoneAdapter ¶
func NewPhoneAdapter(config msg.ChannelConfig) *PhoneAdapter
NewPhoneAdapter creates a Twilio SMS adapter from the config.
func (*PhoneAdapter) InboundCh ¶
func (p *PhoneAdapter) InboundCh() <-chan InboundMessage
InboundCh returns the channel for receiving inbound SMS messages.
func (*PhoneAdapter) RecentMessages ¶
func (p *PhoneAdapter) RecentMessages(count int) []PhoneMessage
RecentMessages returns up to count of the most recent received messages (oldest first, newest last). count<=0 returns all retained messages.
func (*PhoneAdapter) Send ¶
func (p *PhoneAdapter) Send(ctx context.Context, outbound OutboundMessage) error
Send delivers a message as one or more SMS. Long content is split under the REST API's 1600-character limit; each chunk is a separate message, which is how a long reply arrives on a phone regardless.
func (*PhoneAdapter) SetReplyMode ¶
func (p *PhoneAdapter) SetReplyMode(_ string)
SetReplyMode is a no-op for SMS: a text message to the business number is always addressed to it, so there is no "mentions" distinction to draw.
func (*PhoneAdapter) Start ¶
func (p *PhoneAdapter) Start(ctx context.Context) error
Start validates credentials and launches the inbound webhook server. The listener binds synchronously so a port conflict surfaces as a Start error rather than a silent goroutine failure.
func (*PhoneAdapter) Status ¶
func (p *PhoneAdapter) Status() msg.ChannelStatus
Status returns the current Twilio connection status.
func (*PhoneAdapter) Stop ¶
func (p *PhoneAdapter) Stop() error
Stop gracefully shuts down the webhook listener.
type PhoneMessage ¶
type PhoneMessage struct {
ID string `json:"id"`
MessageSid string `json:"message_sid"`
From string `json:"from"` // sender number, E.164
To string `json:"to"` // the Twilio number that received it
Text string `json:"text"`
NumMedia int `json:"num_media,omitempty"`
Time time.Time `json:"time"`
}
PhoneMessage is a received inbound SMS retained for the human-governed draft/approve flow. ID is a short handle; MessageSid is Twilio's SMxxx id.
type SignalAdapter ¶
type SignalAdapter struct {
// contains filtered or unexported fields
}
SignalAdapter bridges Signal (via a signal-cli daemon) to the humanoid actor system.
func NewSignalAdapter ¶
func NewSignalAdapter(config msg.ChannelConfig) *SignalAdapter
NewSignalAdapter creates a Signal channel adapter from the config.
func (*SignalAdapter) InboundCh ¶
func (s *SignalAdapter) InboundCh() <-chan InboundMessage
InboundCh returns the inbound message channel.
func (*SignalAdapter) PairingCh ¶
func (s *SignalAdapter) PairingCh() <-chan PairingEvent
PairingCh implements PairingChannel: it emits device-link lifecycle events (qr / linked / error) while the signal-cli link flow runs (X4, design 009).
func (*SignalAdapter) Send ¶
func (s *SignalAdapter) Send(ctx context.Context, outbound OutboundMessage) error
Send delivers an outbound message via a JSON-RPC "send" to the daemon.
func (*SignalAdapter) SetReplyMode ¶
func (s *SignalAdapter) SetReplyMode(mode string)
SetReplyMode changes the reply mode at runtime. 1:1 messages are always processed; in "mentions" mode group messages that do not @-mention the linked number are forwarded observe_only (mirroring Slack).
func (*SignalAdapter) Start ¶
func (s *SignalAdapter) Start(ctx context.Context) error
Start dials the signal-cli JSON-RPC endpoint, spawning the daemon first when SidecarCmd is configured, and launches the reader/reconnect loop.
func (*SignalAdapter) Status ¶
func (s *SignalAdapter) Status() msg.ChannelStatus
Status reports the connection status.
func (*SignalAdapter) Stop ¶
func (s *SignalAdapter) Stop() error
Stop closes the sidecar connection and kills the daemon if rysh spawned it.
func (*SignalAdapter) TriggerLink ¶
func (s *SignalAdapter) TriggerLink(force bool) error
TriggerLink implements LinkableChannel: it starts the device-link flow on demand (`##humanoid pair link`, design 009 §3.4). It returns immediately — progress and the re-link-guard refusal both arrive as pairing events. The only synchronous errors are "not connected" and "already linking".
type SlackAdapter ¶
type SlackAdapter struct {
// contains filtered or unexported fields
}
SlackAdapter implements the ChannelAdapter interface for Slack using Socket Mode. It listens for messages on configured channels and replies in-thread.
func NewSlackAdapter ¶
func NewSlackAdapter(config msg.ChannelConfig) *SlackAdapter
NewSlackAdapter creates a new Slack adapter with the given configuration.
func (*SlackAdapter) GetMessage ¶
func (s *SlackAdapter) GetMessage(id string) (SlackMessage, bool)
GetMessage looks up a retained message by its short ID ("a3f9") or its ts. Matching is case-insensitive so an ID typed into a prompt resolves regardless of case.
func (*SlackAdapter) InboundCh ¶
func (s *SlackAdapter) InboundCh() <-chan InboundMessage
InboundCh returns the channel for receiving inbound Slack messages.
func (*SlackAdapter) RecentMessages ¶
func (s *SlackAdapter) RecentMessages(count int) []SlackMessage
RecentMessages returns up to count of the most recent received messages (oldest first, newest last). count<=0 returns all retained messages.
func (*SlackAdapter) Send ¶
func (s *SlackAdapter) Send(_ context.Context, outbound OutboundMessage) error
Send delivers a message back to Slack, replying in-thread if a ThreadID is set.
func (*SlackAdapter) SetReplyMode ¶
func (s *SlackAdapter) SetReplyMode(mode string)
SetReplyMode changes the reply mode at runtime. "messages" responds to all channel messages, "mentions" only to @mentions.
func (*SlackAdapter) Start ¶
func (s *SlackAdapter) Start(ctx context.Context) error
Start connects to Slack via Socket Mode and begins listening for messages.
func (*SlackAdapter) Status ¶
func (s *SlackAdapter) Status() msg.ChannelStatus
Status returns the current Slack connection status.
func (*SlackAdapter) Stop ¶
func (s *SlackAdapter) Stop() error
Stop gracefully disconnects from Slack.
func (*SlackAdapter) Validate ¶
func (s *SlackAdapter) Validate(ctx context.Context) error
Validate checks the Slack credentials with a single auth.test call — the same call Start() makes first — without opening the Socket Mode connection. It verifies the bot token remotely; the app-level token is only checked for presence/shape (Slack offers no cheap non-binding check for app tokens — they are exercised when the socket connects).
type SlackMessage ¶
type SlackMessage struct {
ID string `json:"id"` // short 4-char handle for tools
From string `json:"from"` // sender user ID
Name string `json:"name"` // sender display name
Channel string `json:"channel"` // channel ID the message arrived in
Text string `json:"text"` //
ThreadTS string `json:"thread_ts"` // thread root ts (reply target)
TS string `json:"ts"` // this message's ts
Time time.Time `json:"time"`
}
SlackMessage is a received Slack message retained for the human-governed list/read tools. Channel + ThreadTS are what a reply needs to post back into the right conversation.
type TeamsAdapter ¶
type TeamsAdapter struct {
// contains filtered or unexported fields
}
TeamsAdapter implements ChannelAdapter for Microsoft Teams.
func NewTeamsAdapter ¶
func NewTeamsAdapter(config msg.ChannelConfig) *TeamsAdapter
NewTeamsAdapter creates a Teams channel adapter from the config.
func (*TeamsAdapter) InboundCh ¶
func (t *TeamsAdapter) InboundCh() <-chan InboundMessage
InboundCh returns the inbound message channel.
func (*TeamsAdapter) Send ¶
func (t *TeamsAdapter) Send(ctx context.Context, outbound OutboundMessage) error
Send delivers a message to a Teams conversation. The conversation is taken from ThreadID when set — the inbound path puts the conversation id there — falling back to RecipientID.
func (*TeamsAdapter) SetReplyMode ¶
func (t *TeamsAdapter) SetReplyMode(mode string)
SetReplyMode changes the reply mode ("messages" or "mentions") at runtime.
func (*TeamsAdapter) Start ¶
func (t *TeamsAdapter) Start(ctx context.Context) error
Start verifies the bot credentials by acquiring a Connector token — the Teams analogue of Slack's auth.test — then binds the Messaging Endpoint listener. Both happen synchronously so a bad secret or a port conflict surfaces as a Start error rather than a silent goroutine failure.
func (*TeamsAdapter) Status ¶
func (t *TeamsAdapter) Status() msg.ChannelStatus
Status reports the connection status.
func (*TeamsAdapter) Stop ¶
func (t *TeamsAdapter) Stop() error
Stop shuts down the messaging endpoint.
type TelegramAdapter ¶
type TelegramAdapter struct {
// contains filtered or unexported fields
}
TelegramAdapter bridges Telegram (Bot API) to the humanoid actor system.
func NewTelegramAdapter ¶
func NewTelegramAdapter(config msg.ChannelConfig) *TelegramAdapter
NewTelegramAdapter creates a Telegram channel adapter from the config.
func (*TelegramAdapter) InboundCh ¶
func (t *TelegramAdapter) InboundCh() <-chan InboundMessage
InboundCh returns the inbound message channel.
func (*TelegramAdapter) Send ¶
func (t *TelegramAdapter) Send(ctx context.Context, outbound OutboundMessage) error
Send delivers an outbound message via sendMessage. The chat (and optional forum topic) is taken from ThreadID when set — it carries the full routing key the inbound path composed — falling back to RecipientID. Long content is split across messages; Kind=="step" renders italic via HTML parse mode.
func (*TelegramAdapter) SetReplyMode ¶
func (t *TelegramAdapter) SetReplyMode(mode string)
SetReplyMode changes the reply mode ("messages" or "mentions") at runtime.
func (*TelegramAdapter) Start ¶
func (t *TelegramAdapter) Start(ctx context.Context) error
Start verifies the bot token via getMe (the Telegram analogue of Slack's auth.test — it also learns the bot's ID and username, needed for mention and reply-to-bot detection), then launches the configured inbound transport.
func (*TelegramAdapter) Status ¶
func (t *TelegramAdapter) Status() msg.ChannelStatus
Status reports the connection status.
func (*TelegramAdapter) Stop ¶
func (t *TelegramAdapter) Stop() error
Stop cancels the poll loop / shuts down the webhook server (the ctx-done goroutine in startWebhook also calls deleteWebhook so Telegram stops delivering to a dead endpoint).
type Validator ¶
Validator is implemented by adapters that can check their credentials WITHOUT establishing a full connection or binding any port/webhook (e.g. Slack auth.test, a WhatsApp Graph token GET). `rysh doctor` prefers this non-binding check; adapters that don't implement it are reported as "not validated" rather than probed via Start(), which has side effects (socket-mode connections, webhook listeners). See design 004 §4.3.
type WhatsAppAdapter ¶
type WhatsAppAdapter struct {
// contains filtered or unexported fields
}
WhatsAppAdapter implements the ChannelAdapter interface for WhatsApp using the Meta Cloud API (WhatsApp Business Platform).
Outbound: a direct POST to the Graph API
https://graph.facebook.com/{version}/{phone_number_id}/messages
with a Bearer access token — no server round-trip required.
Inbound: Meta delivers messages via webhook to a public HTTPS endpoint, so the adapter runs a small local HTTP server (on WebhookPort). The user exposes that port publicly (a reverse proxy / tunnel) and registers the URL as the app's Callback URL in the Meta dashboard. The server answers the GET verification handshake and validates the X-Hub-Signature-256 HMAC on inbound POSTs.
func NewWhatsAppAdapter ¶
func NewWhatsAppAdapter(config msg.ChannelConfig) *WhatsAppAdapter
NewWhatsAppAdapter creates a new WhatsApp adapter with the given configuration.
func (*WhatsAppAdapter) GetMessage ¶
func (w *WhatsAppAdapter) GetMessage(id string) (WhatsAppMessage, bool)
GetMessage looks up a retained message by its short ID ("a3f9") or its wamid. Matching is case-insensitive so an ID typed into a prompt resolves regardless of case.
func (*WhatsAppAdapter) InboundCh ¶
func (w *WhatsAppAdapter) InboundCh() <-chan InboundMessage
InboundCh returns the channel for receiving inbound WhatsApp messages.
func (*WhatsAppAdapter) RecentMessages ¶
func (w *WhatsAppAdapter) RecentMessages(count int) []WhatsAppMessage
RecentMessages returns up to count of the most recent received messages (oldest first, newest last). count<=0 returns all retained messages.
func (*WhatsAppAdapter) RelayMode ¶
func (w *WhatsAppAdapter) RelayMode() bool
RelayMode reports whether the adapter routes through rysh-server's channel relay rather than its own webhook + Graph API.
func (*WhatsAppAdapter) Send ¶
func (w *WhatsAppAdapter) Send(ctx context.Context, outbound OutboundMessage) error
Send delivers a message via the WhatsApp Cloud API. Inside the 24h session window it sends free-form text (split across messages when over the Cloud API limit). Outside the window free-form text is undeliverable, so it falls back to the configured re-engagement template — or fails loudly when none is configured, rather than attempting a send Meta would reject.
func (*WhatsAppAdapter) SendTemplate ¶
func (w *WhatsAppAdapter) SendTemplate(ctx context.Context, to, name, lang string, bodyParams []string) error
SendTemplate is the exported entry point used by the human-governed whatsapp_send_template tool: an explicit, approved template send. The template must already be approved in Meta Business Manager and the recipient must have opted in — the Cloud API enforces both; we surface its errors.
func (*WhatsAppAdapter) SetConfig ¶
func (w *WhatsAppAdapter) SetConfig(cfg msg.ChannelConfig)
SetConfig replaces the adapter's configuration before Start.
The humanoid pre-creates this adapter at construction time (so the whatsapp_* tools and the running channel share one message store) but only resolves server-side credentials later, in startChannel. Without this the resolved config was computed and thrown away, and relay mode started with no upstream url, key or connection id.
func (*WhatsAppAdapter) SetReplyMode ¶
func (w *WhatsAppAdapter) SetReplyMode(_ string)
SetReplyMode is a no-op for WhatsApp (all inbound direct messages are processed).
func (*WhatsAppAdapter) Start ¶
func (w *WhatsAppAdapter) Start(ctx context.Context) error
Start validates credentials and launches the inbound webhook server.
func (*WhatsAppAdapter) Status ¶
func (w *WhatsAppAdapter) Status() msg.ChannelStatus
Status returns the current WhatsApp connection status.
func (*WhatsAppAdapter) Stop ¶
func (w *WhatsAppAdapter) Stop() error
Stop gracefully shuts down the webhook listener.
func (*WhatsAppAdapter) Validate ¶
func (w *WhatsAppAdapter) Validate(ctx context.Context) error
Validate checks the WhatsApp Cloud credentials with a lightweight Graph GET on the configured phone_number_id — no webhook listener is bound. A 401/403 is decoded as a rejected/expired token; other non-2xx statuses surface the Graph error message.
type WhatsAppMessage ¶
type WhatsAppMessage struct {
ID string `json:"id"`
MessageID string `json:"message_id"`
From string `json:"from"` // sender wa_id (phone number)
Name string `json:"name"`
Text string `json:"text"`
Time time.Time `json:"time"`
}
WhatsAppMessage is a received inbound message retained for the human-governed list/read tools. ID is a short, human-friendly handle ("wa-1"); MessageID is the underlying WhatsApp wamid.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package plugin implements the out-of-process channel plugin SDK (openclaw_roadmap design 002, WS2 P1-P3): a third party ships a channel as a separate process speaking a small wire contract, and the in-core PluginChannelAdapter shim proxies the ChannelAdapter interface to it over stdio JSON-RPC (fallback) or the embedded per-session NATS bus (preferred).
|
Package plugin implements the out-of-process channel plugin SDK (openclaw_roadmap design 002, WS2 P1-P3): a third party ships a channel as a separate process speaking a small wire contract, and the in-core PluginChannelAdapter shim proxies the ChannelAdapter interface to it over stdio JSON-RPC (fallback) or the embedded per-session NATS bus (preferred). |