signal-osint-agent is a production-minded local MVP for a Signal-connected assistant that ingests approved group chat traffic, answers authorized direct messages privately, and combines group memory, local knowledge files, OpenAI Responses API generation, and OpenAI built-in web search.
signal-osint-agent/
├── app/
│ ├── api/
│ ├── core/
│ ├── db/
│ ├── models/
│ ├── schemas/
│ ├── services/
│ ├── cli.py
│ └── main.py
├── knowledge/
├── migrations/
├── scripts/
├── tests/
├── .env.example
├── alembic.ini
├── ARCHITECTURE.md
├── docker-compose.yml
├── Dockerfile
├── Makefile
├── README.md
├── RUNBOOK.md
├── SECURITY_NOTES.md
└── pyproject.toml
- Accepts Signal webhook payloads and normalizes them into internal message objects.
- Stores and ingests group chat messages into PostgreSQL-backed memory.
- Maintains best-effort membership observations from observed group traffic.
- Allows direct-message responses only for users seen in at least one
authorizedgroup. - Refuses unauthorized DMs politely.
- Keeps direct-message content out of shared retrieval memory.
- Supports local knowledge ingestion for
.txt,.md, and.pdf. - Queues ingestion jobs and processes them via a background worker.
- Optionally auto-replies in authorized groups when enabled, with rate limits.
- Uses OpenAI Responses API for answer generation and built-in
web_searchwhen enabled.
- Python 3.12
- FastAPI
- uv package workflow inside the image
- PostgreSQL
- pgvector image for Postgres initialization
- SQLAlchemy + Alembic
- pytest
- Docker Compose
- signal-cli-rest-api
Install Docker Desktop on the laptop and confirm both docker and docker compose work.
Install Python 3.12 locally if you want to run tests or scripts outside containers.
Copy .env.example to .env and fill in:
OPENAI_API_KEYDATABASE_URLSIGNAL_API_BASE_URLSIGNAL_BOT_NUMBERSIGNAL_WEBHOOK_SECRETADMIN_API_TOKENADMIN_API_TOKENS(optional, comma-separated)- If you are not running the worker, set
INGESTION_WORKER_ENABLED=falseto use background tasks. - To enable group auto-replies, set
ENABLE_GROUP_AUTO_REPLY=true(optionallyGROUP_AUTO_REPLY_REQUIRE_QUESTION=true).
Run:
make bootstrapThis script checks Docker, validates required env vars, starts Postgres and signal-cli-rest-api, waits briefly, runs Alembic migrations, and prints next steps.
Run:
make devThis starts the API and the ingestion worker containers.
This deployment assumes the bot gets a brand-new Signal account whose only active device is the laptop-hosted signal-cli-rest-api stack.
You still need a phone number that can receive the Signal verification code, but you do not need to keep a phone logged in as the primary Signal client after registration.
Recommended operator flow:
- Obtain a fresh phone number for the bot account.
- Start the Compose stack so
signal-cli-rest-apiis reachable onhttp://localhost:8080. - Use the registration endpoints exposed by your installed
signal-cli-rest-apiversion to start primary-device registration forSIGNAL_BOT_NUMBER. - Receive the verification code by SMS or voice on that phone number.
- Submit the verification code through the REST API to complete account registration.
- Confirm the account is now registered on the laptop-hosted Signal stack.
Typical signal-cli-rest-api flows expose endpoints in this family:
POST /v1/register/{number}POST /v1/register/{number}/verify/{code}- account/status helpers under
/v1/accountsor similar
The exact paths can vary by image release, so verify them against the version of bbernhard/signal-cli-rest-api you are running.
Important:
- This primary-device automation path is based on unofficial tooling.
- Keeping
signal-cli-rest-apicurrent matters. - Future Signal changes can require transport maintenance.
- A fresh bot number is strongly preferred over reusing a personal Signal account.
After registration, use the newly registered bot account on the laptop-hosted stack, add it to a group, send a message, and check:
curl -H "Authorization: Bearer $ADMIN_API_TOKEN" http://localhost:8000/admin/groupscurl -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" \
http://localhost:8000/admin/groups/<group_id>/authorizecurl http://localhost:8000/healthcurl -H "Authorization: Bearer $ADMIN_API_TOKEN" \
http://localhost:8000/admin/ingestion/healthIf the sender has recent membership observations in an authorized group, the bot retrieves allowed group memory plus knowledge, optionally uses web search, and replies privately.
If the sender is only in ingest_only groups or is unseen, the bot sends a polite refusal and does not ingest the DM into shared memory.
GET /healthGET /admin/groupsPOST /admin/groups/{group_id}/authorizePOST /admin/groups/{group_id}/ingest-onlyPOST /admin/groups/{group_id}/ignorePOST /admin/reindexPOST /admin/knowledge/uploadGET /admin/users/{user_id}/accessGET /admin/ingestion/healthPOST /webhooks/signal
python -m app.cli init-db
python -m app.cli reembed <group_id>
python -m app.cli add-knowledge ./knowledge/example.md --collection-scope globalmake ingest-demoThis seeds two groups, one authorized, one ingest_only, one allowed user, and one denied user.
make testThe Compose database image includes pgvector and the Alembic migration initializes the extension. Production retrieval now uses native pgvector columns plus cosine-distance ordering in PostgreSQL. The SQLite test path still falls back to JSON-backed embeddings and application-side scoring so the test suite can run without Postgres.
This repo now assumes a laptop-hosted primary Signal account for the bot, not a linked secondary desktop. The laptop/container stack is the bot's only active Signal device. That is operationally convenient for later VPS migration, but it depends on unofficial tooling and should be treated as a maintenance-sensitive integration.