howl/SETUP.md
lorentz 3bfda9e585 Initial commit: Howl M365 email management daemon
Full implementation of the Howl email triage system:
- Microsoft Graph API integration with MSAL auth (client-credentials and delegated modes)
- Claude LLM classification via tool use for structured output
- PostgreSQL database with customers, vendors, whitelist, and email_log tables
- Alembic migration for full schema
- APScheduler daemon with graceful shutdown
- Typer CLI (run, dry-run, status commands)
- Business rule classifier with overrides (whitelist protection, low-confidence fallback)
- Action executor (move to folders, flag, escalate with webhook)
- 35 passing unit tests
- README, SETUP, and NEXT_STEPS documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 16:02:24 -04:00

275 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Setup Guide
## Prerequisites
- Python 3.11+
- PostgreSQL 14+ database (local or hosted)
- Microsoft 365 account with an Azure AD app registration
- Anthropic API key
---
## 1. Azure AD App Registration
### For a shared mailbox (recommended for production)
This uses **application permissions** — the daemon runs without any user logged in.
1. Go to [portal.azure.com](https://portal.azure.com) → **Azure Active Directory****App registrations****New registration**
2. Name it something like `howl-email-daemon`; leave the redirect URI blank
3. After creation, note the **Application (client) ID** and **Directory (tenant) ID**
4. Go to **Certificates & secrets****New client secret** → copy the value immediately
5. Go to **API permissions****Add a permission****Microsoft Graph****Application permissions**
- Add `Mail.Read`
- Add `Mail.ReadWrite`
6. Click **Grant admin consent** (requires a Global Admin)
Set in `.env`:
```
GRAPH_AUTH_MODE=client_credentials
AZURE_TENANT_ID=<Directory (tenant) ID>
AZURE_CLIENT_ID=<Application (client) ID>
AZURE_CLIENT_SECRET=<client secret value>
GRAPH_MAILBOX=shared@yourcompany.com
```
### For a personal mailbox
This uses **delegated permissions** — the daemon acts on behalf of a user.
1. Follow steps 14 above
2. Go to **API permissions****Add a permission****Microsoft Graph****Delegated permissions**
- Add `Mail.Read`
- Add `Mail.ReadWrite`
3. Under **Authentication****Advanced settings**, enable **Allow public client flows**
Set in `.env`:
```
GRAPH_AUTH_MODE=delegated
AZURE_TENANT_ID=<Directory (tenant) ID>
AZURE_CLIENT_ID=<Application (client) ID>
# No AZURE_CLIENT_SECRET needed for delegated mode
GRAPH_MAILBOX=yourname@yourcompany.com
MSAL_TOKEN_CACHE_PATH=.msal_cache.bin
```
On first `howl run`, you'll be shown a device code URL and prompted to log in. The token is cached to `MSAL_TOKEN_CACHE_PATH` and refreshes automatically.
---
## 2. PostgreSQL Database
Create a database for Howl:
```sql
CREATE DATABASE howl;
CREATE USER howl_user WITH PASSWORD 'your-password';
GRANT ALL PRIVILEGES ON DATABASE howl TO howl_user;
```
Set in `.env`:
```
DATABASE_URL=postgresql+asyncpg://howl_user:your-password@localhost:5432/howl
```
Run migrations to create all tables:
```bash
alembic upgrade head
```
---
## 3. Environment Configuration
Copy the template and fill in your values:
```bash
cp .env.example .env
```
### Required settings
| Variable | Description |
|---|---|
| `AZURE_TENANT_ID` | Azure AD directory (tenant) ID |
| `AZURE_CLIENT_ID` | App registration client ID |
| `AZURE_CLIENT_SECRET` | Client secret (client_credentials mode only) |
| `GRAPH_MAILBOX` | Email address of the mailbox to monitor |
| `ANTHROPIC_API_KEY` | Your Anthropic API key |
| `DATABASE_URL` | PostgreSQL connection string (asyncpg driver) |
### Key optional settings
| Variable | Default | Description |
|---|---|---|
| `GRAPH_AUTH_MODE` | `client_credentials` | `client_credentials` or `delegated` |
| `GRAPH_POLL_INTERVAL_SECONDS` | `300` | How often to check for new mail (seconds) |
| `GRAPH_BATCH_SIZE` | `50` | Messages fetched per poll cycle |
| `GRAPH_MAX_BODY_CHARS` | `4000` | Email body characters sent to LLM (truncated beyond this) |
| `ANTHROPIC_MODEL` | `claude-sonnet-4-6` | Claude model to use |
| `LLM_CONFIDENCE_THRESHOLD` | `0.60` | Below this, email goes to Needs Review |
| `DRY_RUN` | `false` | Set `true` to classify without touching email |
| `MAX_RETRY_COUNT` | `3` | Retry attempts for failed messages |
| `NOTIFICATION_WEBHOOK_URL` | _(empty)_ | Teams or Slack webhook for escalation alerts |
### Mail folder names
These are the display names Howl will create/find in the monitored mailbox:
| Variable | Default |
|---|---|
| `FOLDER_CUSTOMERS` | `Customers` |
| `FOLDER_VENDORS` | `Vendors` |
| `FOLDER_REVIEW` | `Needs Review` |
| `FOLDER_SPAM` | `Junk Email` |
| `FOLDER_ESCALATE` | `Escalate` |
Folders are created automatically on first use if they don't already exist.
---
## 4. Populate the Database
Add your contacts before running the daemon. Howl matches sender addresses against these tables to provide context to the LLM.
### Adding customers
```sql
-- Insert a customer
INSERT INTO customers (name, company, notes)
VALUES ('Alice Johnson', 'Acme Corp', 'Key account — priority response required');
-- Attach email addresses
INSERT INTO customer_emails (customer_id, email_address, is_primary)
VALUES (
(SELECT id FROM customers WHERE name = 'Alice Johnson'),
'alice@acme.com',
TRUE
);
```
### Adding vendors
```sql
INSERT INTO vendors (name, company, service_category)
VALUES ('SupplyPro', 'SupplyPro Inc', 'supplier');
INSERT INTO vendor_emails (vendor_id, email_address, is_primary)
VALUES (
(SELECT id FROM vendors WHERE name = 'SupplyPro'),
'billing@supplypro.com',
TRUE
);
```
### Adding whitelist entries
```sql
-- Exact email address
INSERT INTO whitelist (email_address, description, added_by)
VALUES ('audit@partnerfirm.com', 'External audit partner', 'admin');
-- Entire domain (matches any @microsoft.com sender)
INSERT INTO whitelist (domain, description, added_by)
VALUES ('microsoft.com', 'Microsoft services', 'admin');
-- Temporary entry (expires in 30 days)
INSERT INTO whitelist (email_address, description, expires_at, added_by)
VALUES ('temp@contractor.com', 'Temp contractor access', NOW() + INTERVAL '30 days', 'admin');
```
---
## 5. Running Howl
### Test first with dry-run
```bash
# Process up to 10 emails, classify them, but take no action
howl dry-run --limit 10
```
Check results:
```bash
# See what classifications were made
howl status --limit 20
# Or query directly
psql -d howl -c "SELECT sender_address, llm_classification, final_action, status FROM email_log ORDER BY created_at DESC LIMIT 10;"
```
### Start the daemon
```bash
howl run
```
The daemon polls every `GRAPH_POLL_INTERVAL_SECONDS` (default: 5 minutes). Stop it with `Ctrl+C` or `SIGTERM` — it finishes the current batch before exiting.
### Running as a systemd service (Linux)
Create `/etc/systemd/system/howl.service`:
```ini
[Unit]
Description=Howl Email Management Daemon
After=network.target postgresql.service
[Service]
Type=simple
User=howl
WorkingDirectory=/opt/projects/howl
EnvironmentFile=/opt/projects/howl/.env
ExecStart=/opt/projects/howl/.venv/bin/howl run
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl enable howl
sudo systemctl start howl
sudo journalctl -u howl -f # Follow logs
```
---
## 6. Webhook Notifications (optional)
Set `NOTIFICATION_WEBHOOK_URL` to receive alerts when an email is escalated.
### Microsoft Teams
1. In Teams, go to the channel → **...** → **Connectors****Incoming Webhook**
2. Create and copy the webhook URL
3. Set `NOTIFICATION_WEBHOOK_URL=<webhook URL>`
### Slack
1. In Slack, go to [api.slack.com/apps](https://api.slack.com/apps) → create an app → **Incoming Webhooks**
2. Activate and add to a channel, copy the URL
3. Set `NOTIFICATION_WEBHOOK_URL=<webhook URL>`
---
## Troubleshooting
**Authentication error (client_credentials)**
- Verify the client secret hasn't expired in the Azure portal
- Ensure admin consent was granted for the application permissions
**401 Unauthorized from Graph**
- For delegated mode: delete `.msal_cache.bin` and re-authenticate with `howl run`
- Check that the app registration has the correct permissions for the mailbox
**Messages not being processed**
- Check `email_log` for `status = 'failed'` rows and read `error_message`
- Ensure the mailbox address in `GRAPH_MAILBOX` exactly matches the M365 UPN or shared mailbox address
**Low LLM accuracy**
- Lower `LLM_CONFIDENCE_THRESHOLD` to move more edge cases to `Needs Review`
- Add `notes` to customer/vendor records — these are included in the LLM context
- Inspect `llm_raw_response` in `email_log` for Claude's reasoning