Skip to main content

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-cli daemon 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:

KeyDefaultMeaning
dm.policyallowlistallowlist issues a pairing code to an unlisted sender. open accepts anyone
dm.allowFrom[]Phone numbers or UUIDs allowed to DM
group.policyallowlistWhich 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.allowFrom and group.allowFrom narrow. ["*"] accepts anyone who can reach the number.

Troubleshooting​

  • Run nanoinfra channels status to confirm nanoinfra sees the channel as enabled.
  • Confirm the daemon answers on daemonHost:daemonPort before you debug nanoinfra.
  • Run nanoinfra gateway --verbose while debugging channel startup.