Production-grade AI inbox triage agent. Hardened with prompt-injection defenses, sender allowlisting, idempotent processing, and fail-closed secret handling. FastAPI · Gmail · LLM · Postgres.
Sentinel-AI watches a Gmail inbox, classifies each unread email into
Meeting / Action / FYI (or Needs-Review when uncertain), applies a
Gmail label, and creates a tentative calendar event for meeting requests.
It refuses to send mail or delete anything. The classifier uses an LLM
with automatic provider failover, falling back to a deterministic keyword
classifier when both providers are down.
Most AI agent tutorials skip the safety patterns that matter when the agent runs against your real inbox in production. Sentinel-AI demonstrates four safety patterns most production agents need:
-
Sender allowlist enforced at fetch time — emails from anyone not on the explicit allowlist never reach the classifier or any outbound action. The trust boundary lives at the entrance, not somewhere deep in the pipeline.
-
Prompt-injection defense — content matching known injection patterns is detected, logged, labeled
Sentinel-Blocked, and dropped before classification. Defense in depth even after the allowlist. -
Idempotent processing — every processed message id is persisted to Postgres. Restarts, retries, double polls — none of them double-process or double-label an email.
-
Fail-closed defaults — when the LLM returns malformed JSON, when both providers fail, when a required env var is missing — the system refuses to act rather than acting on bad data. Untrusted output gets
Sentinel-Needs-Reviewfor human review.
Plus: PII hashing in logs, no send/delete capability anywhere,
non-root containers with read-only root filesystems, structured JSON
logs with trace IDs, Prometheus metrics, rate-limited API with key
rotation support, and a runbook.
┌────────────┐
cron ───→ │ FastAPI │ ── /healthz /readyz /metrics (no auth)
│ │ ── /heartbeat /digest /urgent /status (auth)
└─────┬──────┘
│ heartbeat()
▼
┌────────────┐
│ Sentinel │
│ Agent │
└─────┬──────┘
│
┌───────────────┼───────────────┬──────────────────┐
▼ ▼ ▼ ▼
Gmail (read Allowlist Injection LLM Classifier
+ label only) (sender filter) Guard ┌─────────────┐
│ Provider 1 │
└─────┬───────┘
│ on fail
▼
┌─────────────┐
│ Provider 2 │
└─────┬───────┘
│ on fail
▼
Keyword fallback
(always returns
needs-review)
│
▼
┌────────────┐
│ Postgres │ processed_emails (idempotency)
└────────────┘ audit_log
Full diagrams (mermaid) in docs/ARCHITECTURE.md.
Operational procedures in docs/RUNBOOK.md.
- Python 3.13
- Postgres 16+ (or
docker run postgres:16-alpine) - A Gmail account + Google Cloud OAuth client (Desktop app type)
- API key for at least one LLM provider (OpenAI-compatible)
git clone https://github.com/lubobali/Sentinel-AI.git
cd Sentinel-AI
python3.13 -m venv venv && source venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env # fill in your valuesPlace your Google OAuth client_secret JSON at secrets/credentials.json.
python scripts/smoke_gmail.py # opens browser, saves secrets/token.jsonuvicorn sentinel.api.main_runner:app --port 8097
# Or with Docker:
docker compose -f docker-compose.prod.yml up -dcurl http://localhost:8097/healthz
curl -H "X-API-Key: $SENTINEL_API_KEY_PRIMARY" http://localhost:8097/statusLive /heartbeat against the deployed Hetzner instance:
All config via environment variables (or .env). See
.env.example for the full list. Highlights:
| Variable | Purpose |
|---|---|
ALLOWED_SENDERS |
Comma-separated allowlist (mandatory) |
URGENT_SENDERS |
Subset that gets Sentinel-Urgent label |
RECIPIENT |
Your own address (ignored from triage) |
PRIMARY_LLM_* |
Primary LLM provider config |
NVIDIA_* |
Fallback LLM provider config |
SENTINEL_API_KEY_PRIMARY |
Required, X-API-Key for app endpoints |
SENTINEL_API_KEY_SECONDARY |
Optional, for zero-downtime rotation |
DRY_RUN |
When true, classify but skip outbound actions |
pytest # all tests
pytest -m fast # fast unit tests only
pytest --cov=sentinel # with coverage- 181 tests, currently passing
- 93% line coverage on business logic
mypy --strictcleanruff checkcleanbandit -llclean (no medium+ security issues)pip-auditclean (no known CVEs)
ruff format . # format
ruff check . # lint
mypy --strict sentinel/ # type-check
pre-commit install # gate every commitCI runs all of the above on every push.
Multi-stage Docker (python:3.13-slim runtime, non-root user, read-only
root filesystem). Postgres in a separate container. Service binds to
127.0.0.1 only — expose via reverse proxy or Tailscale, not directly to
the internet.
See docs/RUNBOOK.md for deploy + rollback +
restore drill procedures.
MIT. See LICENSE.




