Build a Signal AI Agent with nanoinfra
Connect nanoinfra to Signal through the signal-cli daemon, so the agent answers on a
Signal number you control.
What you will build
- a working local nanoinfra reply
- a
signal-clidaemon nanoinfra talks to over HTTP - one Signal number that reaches the agent
When to use this
Use Signal when the channel itself must be end-to-end encrypted, or when the people who should reach the agent already use Signal and nothing else.
Signal differs from the other channels in one way worth knowing before you start: there is no bot account. nanoinfra acts as a registered Signal number, so the number needs to be one you can register and one nobody else uses.
Install
Quick Start ranks four install methods easiest first. This is the first of them. Use pip, Docker or a source checkout instead if you prefer, and come back here.
uv tool install nanoinfra
nanoinfra onboard --wizard
nanoinfra agent -m "Hello!"
Signal needs its optional dependency:
nanoinfra plugins enable signal
Run the signal-cli daemon
nanoinfra does not speak the Signal protocol itself. It talks HTTP to a
signal-cli-rest-api daemon, which
holds the registration.
Register the number with that daemon first, and confirm it answers before configuring nanoinfra. A daemon that is not registered looks exactly like a misconfigured channel.
Minimal working example
{
"channels": {
"signal": {
"enabled": true,
"phoneNumber": "+15551234567",
"daemonHost": "localhost",
"daemonPort": 8080
}
}
}
phoneNumber is the only required field. daemonHost defaults to localhost and
daemonPort to 8080, so a daemon on the same machine needs neither.
Start the gateway, then send the number a private message.
It should return a pairing code. Approve that code from a trusted local surface, and not the example below:
nanoinfra agent -m "/pairing approve <the code the bot sent you>"
If you missed the code, list the pending requests:
nanoinfra agent -m "/pairing"
Access control
Signal uses nested policy keys, not the flat groupPolicy the other channels use:
| Key | Default | Meaning |
|---|---|---|
dm.policy | allowlist | allowlist issues a pairing code to an unlisted sender. open accepts anyone |
dm.allowFrom | [] | Phone numbers or UUIDs allowed to DM |
group.policy | allowlist | Which groups the agent operates in |
group.allowFrom | [] | Group IDs allowed when the policy is allowlist |
group.requireMention | — | Whether a group message needs an @mention |
Both policies already default to allowlist, so Signal issues pairing codes without any
extra setting. That is the opposite of Slack and Mattermost, which default to open.
Production notes
- Keep the daemon and the gateway on the same host, or bind the daemon to a private interface. It holds the registration.
- Restart the gateway after you edit
config.json.
Security notes
- The daemon holds the Signal registration. Treat its host as you would a credential store.
- Keep
dm.allowFromandgroup.allowFromnarrow.["*"]accepts anyone who can reach the number.
Troubleshooting
- Run
nanoinfra channels statusto confirm nanoinfra sees the channel as enabled. - Confirm the daemon answers on
daemonHost:daemonPortbefore you debug nanoinfra. - Run
nanoinfra gateway --verbosewhile debugging channel startup.