docs: update project docs, NC Talk guide, Tina persona, and gitignore
- CLAUDE.md: add new auth/onboarding files to directory map, update security section (JWT/bcrypt/invite details), expand recently completed - README.md: fix Web UI auth description, add User Management section - TODO__Agents.md: mark NC Talk docs and auth/onboarding complete, update Holly onboarding plan to reflect single-instance multi-user approach - docs/NEXTCLOUD_TALK_BOT.md: complete guide — occ commands, nginx config, clarify incoming vs outgoing HMAC difference, multi-user note, full troubleshooting table - home/holly/persona/tina/: flesh out all four persona files with real content (DCC name origin, metal music, reading, foster cats, Holly's profile) - .gitignore: exclude home/**/auth.json, invite.json, profile.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
Inara is registered as a bot in Nextcloud Talk, receiving messages via webhook and replying through the bot API.
|
||||
|
||||
**Status:** Live and confirmed working (2026-03-20)
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
@@ -16,50 +18,86 @@ docker exec -it --user www-data <nc-app-container> php /var/www/html/occ talk:bo
|
||||
--feature webhook --feature response --feature reaction
|
||||
```
|
||||
|
||||
After installing, enable the bot in each Talk conversation via the conversation settings UI.
|
||||
After installing, enable the bot in each Talk conversation via the conversation settings UI (three-dot menu → Bots).
|
||||
|
||||
To list installed bots and verify registration:
|
||||
|
||||
```bash
|
||||
docker exec -it --user www-data <nc-app-container> php /var/www/html/occ talk:bot:list
|
||||
```
|
||||
|
||||
To uninstall (if re-registering with a new secret):
|
||||
|
||||
```bash
|
||||
docker exec -it --user www-data <nc-app-container> php /var/www/html/occ talk:bot:remove <bot-id>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration (cortex/.env)
|
||||
## Configuration
|
||||
|
||||
**`cortex/.env`:**
|
||||
```
|
||||
NEXTCLOUD_URL=https://cloud.dgrzone.com
|
||||
NEXTCLOUD_TALK_BOT_SECRET=<shared secret — must match occ install command>
|
||||
```
|
||||
|
||||
`NEXTCLOUD_URL` defaults to `https://cloud.dgrzone.com` in `config.py`.
|
||||
|
||||
**Nginx:** The `/inara-nextcloud-talk-webhook` endpoint must be reachable by Nextcloud without basic auth. Add a location block before the default `auth_basic` block:
|
||||
|
||||
```nginx
|
||||
location = /inara-nextcloud-talk-webhook {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
```
|
||||
|
||||
(The `/channels/` prefix is already bypassed for Google Chat — consider moving the webhook path to `/channels/nextcloud` in a future cleanup to unify the nginx config.)
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
1. User sends a message in Talk → Nextcloud POSTs signed webhook to `/inara-nextcloud-talk-webhook`
|
||||
2. Cortex verifies the incoming HMAC signature, extracts the message, runs it through the LLM
|
||||
3. Cortex POSTs the reply to `/ocs/v2.php/apps/spreed/api/v1/bot/{token}/message`
|
||||
1. User sends a message in Talk → Nextcloud POSTs a signed webhook to `/inara-nextcloud-talk-webhook`
|
||||
2. Cortex verifies the incoming HMAC signature, extracts the message text, runs it through the LLM
|
||||
3. Cortex POSTs the reply to `/ocs/v2.php/apps/spreed/api/v1/bot/{token}/message` with its own HMAC signature
|
||||
4. The webhook handler returns HTTP 200 immediately; the LLM call happens in a `BackgroundTask` (prevents Nextcloud from disabling the bot due to slow response)
|
||||
|
||||
---
|
||||
|
||||
## HMAC Signing — Critical Detail
|
||||
|
||||
**The signature covers `random + message_text`, NOT `random + raw_body`.**
|
||||
**Incoming and outgoing signatures use different message construction.** This is the most common source of 401 errors.
|
||||
|
||||
This differs from typical webhook protocols. Nextcloud Talk's `BotController::sendMessage` passes the *parsed `$message` parameter* to `ChecksumVerificationService::validateRequest`, not the raw request body.
|
||||
### Incoming (Nextcloud → Cortex)
|
||||
|
||||
Source: `spreed/lib/Controller/BotController.php` → `getBotFromHeaders()`:
|
||||
```php
|
||||
$this->checksumVerificationService->validateRequest($random, $checksum, $secret, $message);
|
||||
// $message is the parsed string, not $request->getContent()
|
||||
```
|
||||
Nextcloud signs its outgoing webhook with `HMAC-SHA256(secret, random + raw_body)`:
|
||||
|
||||
Correct Python:
|
||||
```python
|
||||
sig = hmac.new(
|
||||
# _verify_signature in nextcloud_talk.py
|
||||
expected = hmac.new(
|
||||
secret.encode(),
|
||||
(random_str + message_text).encode("utf-8"), # message_text only, not full body
|
||||
(random_header + body.decode("utf-8")).encode(),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
```
|
||||
|
||||
Wrong (causes persistent 401):
|
||||
### Outgoing (Cortex → Nextcloud)
|
||||
|
||||
When Cortex posts a reply, Nextcloud verifies the signature against the *parsed message string*, not the raw body. This is because `BotController::sendMessage` passes the parsed `$message` parameter to `checksumVerificationService::validateRequest`, not `$request->getContent()`.
|
||||
|
||||
```python
|
||||
# _send_reply in nextcloud_talk.py
|
||||
sig = hmac.new(
|
||||
secret.encode(),
|
||||
(random_str + message).encode("utf-8"), # message text only, NOT json.dumps({"message": ...})
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
```
|
||||
|
||||
Wrong (causes persistent outgoing 401):
|
||||
```python
|
||||
# DON'T sign the full JSON body:
|
||||
sig = hmac.new(secret.encode(), (random_str + '{"message": "..."}').encode(), hashlib.sha256).hexdigest()
|
||||
@@ -67,6 +105,12 @@ sig = hmac.new(secret.encode(), (random_str + '{"message": "..."}').encode(), ha
|
||||
|
||||
---
|
||||
|
||||
## Multi-User Note
|
||||
|
||||
NC Talk currently uses the **default user and persona** (`settings.default_tier`, `load_context()`). All Talk conversations go to Inara regardless of who is messaging. Per-conversation persona routing (e.g., Holly gets Tina) is a future enhancement — would require mapping Nextcloud user IDs or conversation tokens to Cortex users.
|
||||
|
||||
---
|
||||
|
||||
## Claude CLI Auth in systemd
|
||||
|
||||
The `CLAUDE_CODE_OAUTH_TOKEN` in `.env` goes stale after each `claude auth login` (tokens rotate). Cortex reads the token live from `~/.claude/.credentials.json` on every Claude call (`llm_client._fresh_claude_token()`), so no manual `.env` update is needed after re-authentication.
|
||||
@@ -75,13 +119,21 @@ Also: never set `ANTHROPIC_API_KEY` to an OAuth token value (`sk-ant-oat01-...`)
|
||||
|
||||
---
|
||||
|
||||
## Triggering the Bot
|
||||
|
||||
- **@mention** — prefix the message with `@inara` (or whatever `AGENT_NAME` is set to); the prefix is stripped before sending to the LLM
|
||||
- **Any message** in a conversation where the bot is enabled — all messages are forwarded, not just @mentions
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| Webhook not received | Bot not enabled for conversation | Enable in Talk conversation settings |
|
||||
| Incoming 401 on webhook | Wrong secret in `.env` | Match secret to `occ talk:bot:install` value |
|
||||
| Reply POST returns 401 | HMAC computed over wrong data | Sign `random + message_text` only |
|
||||
| Reply POST returns 401 (persistent) | Brute force protection | `occ security:bruteforce:reset <cortex-IP>` |
|
||||
| Webhook not received | Bot not enabled for conversation | Enable in Talk conversation settings (Bots) |
|
||||
| Incoming 401 | Wrong secret in `.env` | Match secret to `occ talk:bot:install` value |
|
||||
| Reply POST returns 401 (first try) | HMAC computed over wrong data | Sign `random + message_text` only (not raw JSON body) |
|
||||
| Reply POST returns 401 (persistent) | Brute force protection triggered | `occ security:bruteforce:reset <cortex-IP>` |
|
||||
| Bot auto-disabled by Nextcloud | Webhook held open too long | Verify `BackgroundTasks` is used — return 200 immediately |
|
||||
| Claude falls back to Gemini | Stale/wrong auth token | Token is auto-refreshed from `~/.claude/.credentials.json`; run `claude auth login` if expired |
|
||||
| Bot auto-disabled by Nextcloud | Webhook held open too long during LLM call | Use `BackgroundTasks` — return 200 immediately |
|
||||
| No response at all | Nginx blocking the path with basic auth | Add a `location =` block before the auth block (see Nginx section above) |
|
||||
|
||||
Reference in New Issue
Block a user