Skip to content

Commit b433a98

Browse files
authored
feat(signal): add signal-cli channel (#841)
1 parent 5b16c77 commit b433a98

44 files changed

Lines changed: 3241 additions & 81 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ default-members = [
4242
"crates/secret-store",
4343
"crates/service-traits",
4444
"crates/sessions",
45+
"crates/signal",
4546
"crates/skills",
4647
"crates/slack",
4748
"crates/swift-bridge",
@@ -100,6 +101,7 @@ members = [
100101
"crates/secret-store",
101102
"crates/service-traits",
102103
"crates/sessions",
104+
"crates/signal",
103105
"crates/skills",
104106
"crates/slack",
105107
"crates/swift-bridge",
@@ -363,6 +365,7 @@ moltis-schema-export = { path = "crates/schema-export" }
363365
moltis-secret-store = { default-features = false, path = "crates/secret-store" }
364366
moltis-service-traits = { path = "crates/service-traits" }
365367
moltis-sessions = { path = "crates/sessions" }
368+
moltis-signal = { path = "crates/signal" }
366369
moltis-skills = { path = "crates/skills" }
367370
moltis-slack = { path = "crates/slack" }
368371
moltis-swift-bridge = { path = "crates/swift-bridge" }

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Moltis recently hit [the front page of Hacker News](https://news.ycombinator.com
2525

2626
**Your hardware** — Runs on a Mac Mini, a Raspberry Pi, or any server you own. One Rust binary, no Node.js, no npm, no runtime.
2727

28-
**Full-featured** — Voice, memory, cross-session recall, automatic edit checkpoints, scheduling, Telegram, Discord, browser automation, MCP servers, SSH or node-backed remote exec, managed deploy keys with host pinning in the web UI, a live Settings → Tools inventory, Cursor-compatible project context, and context-file threat scanning — all built-in. No plugin marketplace to get supply-chain attacked through.
28+
**Full-featured** — Voice, memory, cross-session recall, automatic edit checkpoints, scheduling, Telegram, Signal, Discord, browser automation, MCP servers, SSH or node-backed remote exec, managed deploy keys with host pinning in the web UI, a live Settings → Tools inventory, Cursor-compatible project context, and context-file threat scanning — all built-in. No plugin marketplace to get supply-chain attacked through.
2929

3030
**Auditable** — The agent runner and model interface fit in ~7.5K lines, with providers in ~19K more. The Rust workspace is ~270K lines across 59 modular crates you can audit independently, with 470+ Rust files containing tests. Unsafe code is isolated to FFI and precompiled runtime boundaries, not the core agent loop.
3131

@@ -90,7 +90,7 @@ Current Rust workspace: ~270K LoC across 59 crates. The table below groups the m
9090

9191
| Category | Crates | Combined LoC |
9292
|----------|--------|-------------|
93-
| Channels | `moltis-telegram`, `moltis-whatsapp`, `moltis-discord`, `moltis-msteams`, `moltis-matrix`, `moltis-slack`, `moltis-nostr`, `moltis-channels` | 34.0K |
93+
| Channels | `moltis-telegram`, `moltis-whatsapp`, `moltis-signal`, `moltis-discord`, `moltis-msteams`, `moltis-matrix`, `moltis-slack`, `moltis-nostr`, `moltis-channels` | 34.0K |
9494
| Web and APIs | `moltis-web`, `moltis-graphql`, `moltis-webhooks` | 10.8K |
9595
| Extensibility | `moltis-mcp`, `moltis-mcp-agent-bridge`, `moltis-skills`, `moltis-plugins` | 11.5K |
9696
| Memory and context | `moltis-memory`, `moltis-qmd`, `moltis-code-index`, `moltis-projects` | 11.7K |
@@ -121,7 +121,7 @@ Verify releases with `gh attestation verify <artifact> -R moltis-org/moltis` or
121121
## Features
122122

123123
- **AI Gateway** — Multi-provider LLM support (OpenAI Codex, GitHub Copilot, Local), streaming responses, agent loop with sub-agent delegation, parallel tool execution
124-
- **Communication** — Web UI, Telegram, Microsoft Teams, Discord, API access, voice I/O (8 TTS + 7 STT providers), mobile PWA with push notifications
124+
- **Communication** — Web UI, Telegram, Signal, Microsoft Teams, Discord, API access, voice I/O (8 TTS + 7 STT providers), mobile PWA with push notifications
125125
- **Memory & Recall** — Per-agent memory workspaces, embeddings-powered long-term memory, hybrid vector + full-text search, session persistence with auto-compaction, cross-session recall, Cursor-compatible project context, context-file safety scanning
126126
- **Safer Agent Editing** — Automatic checkpoints before built-in skill and memory mutations, restore tooling, session branching
127127
- **Extensibility** — MCP servers (stdio + HTTP/SSE), skill system, 15 lifecycle hook events with circuit breaker, destructive command guard

crates/channels/src/plugin.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub enum ChannelType {
2323
Slack,
2424
Matrix,
2525
Nostr,
26+
Signal,
2627
}
2728

2829
impl ChannelType {
@@ -36,6 +37,7 @@ impl ChannelType {
3637
Self::Slack => "slack",
3738
Self::Matrix => "matrix",
3839
Self::Nostr => "nostr",
40+
Self::Signal => "signal",
3941
}
4042
}
4143

@@ -49,6 +51,7 @@ impl ChannelType {
4951
Self::Slack => "Slack",
5052
Self::Matrix => "Matrix",
5153
Self::Nostr => "Nostr",
54+
Self::Signal => "Signal",
5255
}
5356
}
5457

@@ -65,6 +68,13 @@ impl ChannelType {
6568
Some("private".to_string())
6669
}
6770
},
71+
Self::Signal => {
72+
if chat_id.starts_with("group:") {
73+
Some("group".to_string())
74+
} else {
75+
Some("direct".to_string())
76+
}
77+
},
6878
Self::Nostr => Some("dm".to_string()),
6979
_ => None,
7080
}
@@ -80,6 +90,7 @@ impl ChannelType {
8090
Self::Slack => &["bot_token", "app_token", "signing_secret"],
8191
Self::Matrix => &["access_token", "password"],
8292
Self::Nostr => &["secret_key"],
93+
Self::Signal => &[],
8394
}
8495
}
8596
}
@@ -102,6 +113,7 @@ impl std::str::FromStr for ChannelType {
102113
"slack" => Ok(Self::Slack),
103114
"matrix" | "element" => Ok(Self::Matrix),
104115
"nostr" => Ok(Self::Nostr),
116+
"signal" => Ok(Self::Signal),
105117
other => Err(Error::invalid_input(format!(
106118
"unknown channel type: {other}"
107119
))),
@@ -119,6 +131,7 @@ impl ChannelType {
119131
Self::Slack,
120132
Self::Matrix,
121133
Self::Nostr,
134+
Self::Signal,
122135
];
123136

124137
/// Returns the static descriptor for this channel type.
@@ -237,6 +250,22 @@ impl ChannelType {
237250
supports_location: false,
238251
},
239252
},
253+
Self::Signal => ChannelDescriptor {
254+
channel_type: *self,
255+
display_name: "Signal",
256+
capabilities: ChannelCapabilities {
257+
inbound_mode: InboundMode::GatewayLoop,
258+
supports_outbound: true,
259+
supports_streaming: false,
260+
supports_interactive: false,
261+
supports_threads: false,
262+
supports_voice_ingest: false,
263+
supports_pairing: false,
264+
supports_otp: true,
265+
supports_reactions: false,
266+
supports_location: false,
267+
},
268+
},
240269
}
241270
}
242271
}

crates/cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ default = [
127127
"prometheus",
128128
"push-notifications",
129129
"qmd",
130+
"signal",
130131
"slack",
131132
"tailscale",
132133
"tls",
@@ -175,6 +176,7 @@ openclaw-import = ["dep:moltis-openclaw-import", "moltis-gateway/openclaw-imp
175176
prometheus = ["moltis-httpd/prometheus"]
176177
push-notifications = ["moltis-httpd/push-notifications", "moltis-web?/push-notifications"]
177178
qmd = ["moltis-gateway/qmd"]
179+
signal = ["moltis-httpd/signal"]
178180
slack = ["moltis-httpd/slack"]
179181
tailscale = ["moltis-httpd/tailscale", "moltis-web?/tailscale"]
180182
tls = ["moltis-httpd/tls"]

crates/config/src/schema/runtime.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ pub struct McpOAuthOverrideEntry {
183183
/// Kept in `moltis-config` (not `moltis-channels`) so the config crate stays
184184
/// independent of the channels crate while still validating channel names.
185185
pub const KNOWN_CHANNEL_TYPES: &[&str] = &[
186-
"telegram", "whatsapp", "msteams", "discord", "slack", "matrix",
186+
"telegram", "whatsapp", "msteams", "discord", "slack", "matrix", "nostr", "signal",
187187
];
188188

189189
/// Per-chat-type tool policy for a channel account.
@@ -227,7 +227,7 @@ pub struct ChannelToolPolicyOverride {
227227
#[serde(default)]
228228
pub struct ChannelsConfig {
229229
/// Which channel types are offered in the web UI (onboarding + channels page).
230-
/// Defaults to `["telegram", "whatsapp", "msteams", "discord", "slack", "matrix", "nostr"]`.
230+
/// Defaults to `["telegram", "whatsapp", "msteams", "discord", "slack", "matrix", "nostr", "signal"]`.
231231
#[serde(
232232
default = "default_channels_offered",
233233
skip_serializing_if = "Vec::is_empty"
@@ -251,6 +251,9 @@ pub struct ChannelsConfig {
251251
/// Nostr DM accounts, keyed by account ID.
252252
#[serde(default)]
253253
pub nostr: HashMap<String, serde_json::Value>,
254+
/// Signal accounts backed by signal-cli, keyed by account ID.
255+
#[serde(default)]
256+
pub signal: HashMap<String, serde_json::Value>,
254257
/// Additional channel types not covered by the named fields above.
255258
///
256259
/// This allows new channel plugins to be configured without changing
@@ -264,14 +267,15 @@ impl ChannelsConfig {
264267
///
265268
/// This is the single source of truth for the set of named channel types.
266269
/// Keep in sync with the struct fields.
267-
fn named_fields(&self) -> [(&str, &HashMap<String, serde_json::Value>); 6] {
270+
fn named_fields(&self) -> [(&str, &HashMap<String, serde_json::Value>); 7] {
268271
[
269272
("telegram", &self.telegram),
270273
("whatsapp", &self.whatsapp),
271274
("msteams", &self.msteams),
272275
("discord", &self.discord),
273276
("slack", &self.slack),
274277
("nostr", &self.nostr),
278+
("signal", &self.signal),
275279
]
276280
}
277281

@@ -314,6 +318,7 @@ fn default_channels_offered() -> Vec<String> {
314318
"slack".into(),
315319
"matrix".into(),
316320
"nostr".into(),
321+
"signal".into(),
317322
]
318323
}
319324

@@ -327,6 +332,7 @@ impl Default for ChannelsConfig {
327332
discord: HashMap::new(),
328333
slack: HashMap::new(),
329334
nostr: HashMap::new(),
335+
signal: HashMap::new(),
330336
extra: HashMap::new(),
331337
}
332338
}

crates/config/src/schema/tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ fn channels_config_defaults_offered() {
382382
"slack".to_string(),
383383
"matrix".to_string(),
384384
"nostr".to_string(),
385+
"signal".to_string(),
385386
]);
386387
}
387388

@@ -396,6 +397,7 @@ fn channels_config_empty_toml_defaults_offered() {
396397
"slack".to_string(),
397398
"matrix".to_string(),
398399
"nostr".to_string(),
400+
"signal".to_string(),
399401
]);
400402
}
401403

crates/config/src/template.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -877,9 +877,8 @@ reset_on_exit = true # Reset serve/funnel when gateway shuts down
877877
878878
[channels]
879879
# Which channel types appear in the web UI's "+ Add Channel" menu.
880-
# Default: ["telegram", "msteams", "discord", "slack", "matrix", "nostr"]
881-
# Add "whatsapp" to enable it in the UI.
882-
# offered = ["telegram", "msteams", "discord", "slack", "matrix", "nostr", "whatsapp"]
880+
# Default: ["telegram", "whatsapp", "msteams", "discord", "slack", "matrix", "nostr", "signal"]
881+
# offered = ["telegram", "msteams", "discord", "slack", "matrix", "nostr", "signal", "whatsapp"]
883882
884883
# WhatsApp linked-device accounts
885884
# [channels.whatsapp.my-bot]
@@ -1023,6 +1022,24 @@ reset_on_exit = true # Reset serve/funnel when gateway shuts down
10231022
# name = "Moltis Bot"
10241023
# about = "AI assistant on Nostr"
10251024
1025+
# Signal accounts via signal-cli daemon
1026+
# Start signal-cli separately, for example:
1027+
# signal-cli --account +15551234567 daemon --http 127.0.0.1:8080
1028+
# [channels.signal.my-bot]
1029+
# account = "+15551234567" # Signal account loaded by signal-cli
1030+
# http_url = "http://127.0.0.1:8080"
1031+
# dm_policy = "allowlist" # "open", "allowlist", or "disabled"
1032+
# allowlist = [] # Allowed sender phone numbers or UUIDs
1033+
# group_policy = "disabled" # "open", "allowlist", or "disabled"
1034+
# group_allowlist = [] # Signal group IDs
1035+
# mention_mode = "mention" # "mention", "always", or "none"
1036+
# ignore_stories = true
1037+
# model = "anthropic/claude-sonnet-4-20250514"
1038+
# model_provider = "anthropic"
1039+
# otp_self_approval = true # OTP self-approval for non-allowlisted DM users
1040+
# otp_cooldown_secs = 300 # Cooldown after 3 failed OTP attempts
1041+
# text_chunk_limit = 4000
1042+
10261043
# ══════════════════════════════════════════════════════════════════════════════
10271044
# HOOKS
10281045
# ══════════════════════════════════════════════════════════════════════════════

crates/config/src/validate/schema_map.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ pub(super) fn build_schema_map() -> KnownKeys {
417417
("slack", Map(Box::new(channel_account()))),
418418
("matrix", Map(Box::new(channel_account()))),
419419
("nostr", Map(Box::new(channel_account()))),
420+
("signal", Map(Box::new(channel_account()))),
420421
]),
421422
}
422423
}),

crates/gateway/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ moltis-routing = { workspace = true }
5252
moltis-secret-store = { workspace = true }
5353
moltis-service-traits = { workspace = true }
5454
moltis-sessions = { workspace = true }
55+
moltis-signal = { optional = true, workspace = true }
5556
moltis-skills = { workspace = true }
5657
moltis-slack = { optional = true, workspace = true }
5758
moltis-tailscale = { optional = true, workspace = true }
@@ -112,6 +113,7 @@ default = [
112113
"prometheus",
113114
"push-notifications",
114115
"qmd",
116+
"signal",
115117
"slack",
116118
"tailscale",
117119
"tls",
@@ -152,12 +154,14 @@ metrics = [
152154
"moltis-metrics/sqlite",
153155
"moltis-msteams/metrics",
154156
"moltis-nostr?/metrics",
157+
"moltis-signal?/metrics",
155158
]
156159
nostr = ["dep:moltis-nostr"]
157160
openclaw-import = ["dep:moltis-openclaw-import"]
158161
prometheus = ["metrics", "moltis-metrics/prometheus"]
159162
push-notifications = ["dep:chrono", "dep:p256", "dep:web-push", "moltis-chat/push-notifications"]
160163
qmd = ["dep:moltis-qmd", "moltis-code-index/qmd"]
164+
signal = ["dep:moltis-signal"]
161165
slack = ["dep:moltis-slack", "moltis-slack/metrics"]
162166
tailscale = ["dep:moltis-tailscale"]
163167
tls = []

0 commit comments

Comments
 (0)