Add multi-bot Docker setup and improve MCP/tool reliability

Document and add multi-bot Docker workflows with env layering scripts, and update agent/tool configuration handling to make MCP/email/calendar behavior more robust for day-to-day operations.

Made-with: Cursor
This commit is contained in:
tanyar09 2026-03-27 13:06:24 -04:00
parent daeeec7756
commit 4f50cfac3c
22 changed files with 2398 additions and 10 deletions

113
DEVELOPMENT_WITH_DOCKER.md Normal file
View File

@ -0,0 +1,113 @@
# Developing Nanobot with Docker
## Current Setup (Production)
**`docker-compose.multi.env.yml`** - Production mode:
- Code is **copied** into Docker image during build
- Changes to source code **NOT** picked up automatically
- Need to rebuild image: `docker compose -f docker-compose.multi.env.yml build`
## Development Setup
**`docker-compose.multi.dev.yml`** - Development mode:
- Source code is **mounted** as volume
- Changes to `nanobot/` directory **picked up automatically**
- Just restart container (no rebuild needed)
## How It Works
### Production Mode (Current)
```bash
# 1. Build image (copies code)
docker compose -f docker-compose.multi.env.yml build
# 2. Run container
docker compose -f docker-compose.multi.env.yml up -d
# 3. Make code changes...
# 4. Changes NOT visible - need to rebuild:
docker compose -f docker-compose.multi.env.yml build
docker compose -f docker-compose.multi.env.yml up -d --force-recreate
```
### Development Mode (Recommended for Development)
```bash
# 1. Build image once (for dependencies)
docker compose -f docker-compose.multi.dev.yml build
# 2. Run container
docker compose -f docker-compose.multi.dev.yml up -d
# 3. Make code changes in venv...
# 4. Changes visible immediately - just restart:
docker compose -f docker-compose.multi.dev.yml restart nanobot-user1
```
## Workflow
### Option 1: Develop in Venv, Test in Docker (Recommended)
```bash
# Terminal 1: Develop in venv
source venv/bin/activate
# Edit code, test locally if needed
nano nanobot/channels/telegram.py
# Terminal 2: Run Docker in dev mode
docker compose -f docker-compose.multi.dev.yml up -d nanobot-user1
# After making changes, restart container:
docker compose -f docker-compose.multi.dev.yml restart nanobot-user1
# Watch logs:
docker logs -f nanobot-user1-dev
```
### Option 2: Rebuild After Changes (Current)
```bash
# Make changes in venv
source venv/bin/activate
nano nanobot/channels/telegram.py
# Rebuild Docker image
docker compose -f docker-compose.multi.env.yml build nanobot-user1
# Recreate container
docker compose -f docker-compose.multi.env.yml up -d --force-recreate nanobot-user1
```
## Important Notes
### Development Mode (`docker-compose.multi.dev.yml`)
- ✅ Changes picked up automatically
- ✅ Faster iteration (no rebuild needed)
- ⚠️ Mounts source code (may have slight performance impact)
- ⚠️ Python needs to reload modules (restart container)
### Production Mode (`docker-compose.multi.env.yml`)
- ✅ Code baked into image (more stable)
- ✅ No performance impact from mounts
- ❌ Need rebuild for every change
- ✅ Better for production deployments
## Quick Reference
```bash
# Development workflow
docker compose -f docker-compose.multi.dev.yml up -d nanobot-user1
# ... make changes ...
docker compose -f docker-compose.multi.dev.yml restart nanobot-user1
# Production workflow
docker compose -f docker-compose.multi.env.yml build nanobot-user1
docker compose -f docker-compose.multi.env.yml up -d --force-recreate nanobot-user1
```
## Which Should You Use?
- **Developing code**: Use `docker-compose.multi.dev.yml`
- **Production/staging**: Use `docker-compose.multi.env.yml`
- **Quick testing**: Use venv directly on host

614
DOCKER_MULTI_BOT_GUIDE.md Normal file
View File

@ -0,0 +1,614 @@
# Docker Multi-Bot Setup Guide
Complete guide for running multiple nanobot instances with Docker, each with its own Telegram bot.
## 📋 Table of Contents
1. [Quick Start](#quick-start)
2. [Architecture Overview](#architecture-overview)
3. [Setup Instructions](#setup-instructions)
4. [Running Bots](#running-bots)
5. [Configuration Management](#configuration-management)
6. [Development Workflow](#development-workflow)
7. [Troubleshooting](#troubleshooting)
8. [Reference](#reference)
---
## Quick Start
### Start Only User1 Bot
```bash
docker compose -f docker-compose.multi.env.yml up -d nanobot-user1
```
### Start All Bots
```bash
docker compose -f docker-compose.multi.env.yml up -d
```
### Stop All Bots
```bash
docker compose -f docker-compose.multi.env.yml down
```
### Stop Specific Bot
```bash
docker compose -f docker-compose.multi.env.yml stop nanobot-user1
```
### View Logs
```bash
docker logs -f nanobot-user1
```
---
## Architecture Overview
### How It Works
Each bot runs in its own Docker container with:
- **Separate config directory**: `~/.nanobot-user1`, `~/.nanobot-user2`, etc.
- **Shared env file**: `.env.shared` (common settings)
- **Bot-specific env file**: `.env.user1`, `.env.user2`, etc. (overrides)
- **Isolated environment**: No conflicts between bots
### File Structure
```
nanobot/
├── .env.shared # Shared settings (API keys, model, etc.)
├── .env.user1 # Bot 1 overrides
├── .env.user2 # Bot 2 overrides
├── .env.user3 # Bot 3 overrides
├── docker-compose.multi.env.yml # Production compose file
├── docker-compose.multi.dev.yml # Development compose file
└── ~/.nanobot-user1/
└── config.json # Bot 1 channel config (Telegram token, allowFrom)
└── ~/.nanobot-user2/
└── config.json # Bot 2 channel config
└── ~/.nanobot-user3/
└── config.json # Bot 3 channel config
```
### Configuration Loading
1. **Docker Compose** loads environment files:
- First: `.env.shared` (shared settings)
- Second: `.env.userX` (bot-specific overrides)
- Later files override earlier ones
2. **Container** mounts config directory:
- Host: `~/.nanobot-user1` → Container: `/root/.nanobot`
3. **Nanobot** loads:
- Environment variables (from Docker env files)
- Config file: `/root/.nanobot/config.json` (mounted from host)
---
## Setup Instructions
### Step 1: Create Environment Files
Run the setup script:
```bash
./env-files-setup.sh
```
This creates:
- `.env.shared` - Shared settings
- `.env.user1`, `.env.user2`, `.env.user3` - Bot-specific overrides
### Step 2: Edit `.env.shared`
Add your shared settings:
```bash
nano .env.shared
```
Example:
```bash
# Provider Settings
NANOBOT_PROVIDERS__CUSTOM__API_KEY=no-key
NANOBOT_PROVIDERS__CUSTOM__API_BASE=http://172.17.0.1:11434/v1
# Agent Settings
NANOBOT_AGENTS__DEFAULTS__MODEL=llama3.1:8b
NANOBOT_AGENTS__DEFAULTS__WORKSPACE=/mnt/data/nanobot
NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.7
```
**Important**: Use `172.17.0.1` (Docker bridge gateway) instead of `localhost` so containers can reach Ollama on the host.
### Step 3: Edit Bot-Specific Env Files
Edit `.env.user1`, `.env.user2`, etc. with bot-specific settings:
```bash
nano .env.user1
```
Example:
```bash
# Telegram Bot Token
NANOBOT_CHANNELS__TELEGRAM__ENABLED=true
NANOBOT_CHANNELS__TELEGRAM__TOKEN=your_bot_token_here
# Email Credentials (if different per bot)
NANOBOT_CHANNELS__EMAIL__IMAP_USERNAME=bot1@example.com
NANOBOT_CHANNELS__EMAIL__IMAP_PASSWORD=password
```
### Step 4: Create Config Files
Run the config creation script:
```bash
./create-bot-configs.sh
```
Or manually create `~/.nanobot-user1/config.json`:
```json
{
"channels": {
"telegram": {
"enabled": true,
"allowFrom": ["YOUR_TELEGRAM_USERNAME"]
},
"email": {
"enabled": true,
"allowFrom": ["email@example.com"]
}
}
}
```
**Note**: `allowFrom` arrays must be in `config.json`, NOT in env files.
### Step 5: Build Docker Image
```bash
docker compose -f docker-compose.multi.env.yml build
```
---
## Running Bots
### Start Commands
```bash
# Start only user1
docker compose -f docker-compose.multi.env.yml up -d nanobot-user1
# Start only user2
docker compose -f docker-compose.multi.env.yml up -d nanobot-user2
# Start only user3
docker compose -f docker-compose.multi.env.yml up -d nanobot-user3
# Start all bots
docker compose -f docker-compose.multi.env.yml up -d
```
### Stop Commands
```bash
# Stop only user1
docker compose -f docker-compose.multi.env.yml stop nanobot-user1
# Stop only user2
docker compose -f docker-compose.multi.env.yml stop nanobot-user2
# Stop only user3
docker compose -f docker-compose.multi.env.yml stop nanobot-user3
# Stop all bots
docker compose -f docker-compose.multi.env.yml down
```
### Restart Commands
```bash
# Restart only user1
docker compose -f docker-compose.multi.env.yml restart nanobot-user1
# Restart all bots
docker compose -f docker-compose.multi.env.yml restart
```
### Status Commands
```bash
# Check what's running
docker compose -f docker-compose.multi.env.yml ps
# View logs for user1
docker logs -f nanobot-user1
# View logs for all bots
docker compose -f docker-compose.multi.env.yml logs -f
# View logs for specific bot
docker compose -f docker-compose.multi.env.yml logs -f nanobot-user1
```
### Remove Commands
```bash
# Stop and remove user1 container
docker compose -f docker-compose.multi.env.yml stop nanobot-user1
docker rm nanobot-user1
# Stop and remove all containers
docker compose -f docker-compose.multi.env.yml down
# Remove containers and volumes (keeps config files)
docker compose -f docker-compose.multi.env.yml down -v
```
---
## Configuration Management
### Updating Shared Settings
Edit `.env.shared`:
```bash
nano .env.shared
```
Restart affected containers:
```bash
docker compose -f docker-compose.multi.env.yml restart
```
### Updating Bot-Specific Settings
Edit `.env.userX`:
```bash
nano .env.user1
```
Restart that specific bot:
```bash
docker compose -f docker-compose.multi.env.yml restart nanobot-user1
```
### Updating Config Files
Edit config file:
```bash
nano ~/.nanobot-user1/config.json
```
Restart container:
```bash
docker restart nanobot-user1
```
### Adding Email to allowFrom
```bash
# Add email to user1's allowFrom
jq '.channels.email.allowFrom += ["newemail@example.com"]' \
~/.nanobot-user1/config.json > ~/.nanobot-user1/config.json.tmp && \
mv ~/.nanobot-user1/config.json.tmp ~/.nanobot-user1/config.json
# Restart container
docker restart nanobot-user1
```
### Environment Variable Format
Nanobot uses Pydantic's `BaseSettings` with:
- Prefix: `NANOBOT_`
- Nested delimiter: `__` (double underscore)
Examples:
- `NANOBOT_PROVIDERS__CUSTOM__API_KEY``providers.custom.apiKey`
- `NANOBOT_CHANNELS__TELEGRAM__TOKEN``channels.telegram.token`
- `NANOBOT_AGENTS__DEFAULTS__MODEL``agents.defaults.model`
---
## Development Workflow
### Option 1: Development Mode (Recommended)
Use `docker-compose.multi.dev.yml` which mounts source code:
```bash
# Start in development mode
docker compose -f docker-compose.multi.dev.yml up -d nanobot-user1
# Make changes in venv
source venv/bin/activate
nano nanobot/channels/telegram.py
# Restart container (picks up changes)
docker compose -f docker-compose.multi.dev.yml restart nanobot-user1
# View logs
docker logs -f nanobot-user1-dev
```
### Option 2: Rebuild After Changes
```bash
# Make changes
source venv/bin/activate
nano nanobot/channels/telegram.py
# Rebuild image
docker compose -f docker-compose.multi.env.yml build nanobot-user1
# Recreate container
docker compose -f docker-compose.multi.env.yml up -d --force-recreate nanobot-user1
```
### Option 3: Run on Host (Not Docker)
```bash
# Activate venv
source venv/bin/activate
# Run directly
nanobot gateway
```
**Note**: Host command uses:
- Config: `~/.nanobot/config.json` (original)
- .env: `.env` in current directory (NOT `.env.shared`)
---
## Troubleshooting
### Bot Not Responding
1. **Check if container is running**:
```bash
docker ps | grep nanobot-user1
```
2. **Check logs**:
```bash
docker logs nanobot-user1 --tail 50
```
3. **Verify Telegram token**:
```bash
grep TELEGRAM__TOKEN .env.user1
```
4. **Check allowFrom**:
```bash
cat ~/.nanobot-user1/config.json | jq '.channels.telegram.allowFrom'
```
### Connection Error (Ollama)
**Problem**: "Connection error" when bot tries to use LLM
**Solution**:
1. Check Ollama is running:
```bash
curl http://localhost:11434/api/tags
```
2. Verify API_BASE in `.env.shared`:
```bash
grep API_BASE .env.shared
```
Should be: `http://172.17.0.1:11434/v1` (NOT `localhost`)
3. Restart container:
```bash
docker restart nanobot-user1
```
### Config Not Loading
1. **Check volume mount**:
```bash
docker inspect nanobot-user1 | grep -A 5 Mounts
```
2. **Verify config exists**:
```bash
ls -lh ~/.nanobot-user1/config.json
```
3. **Check inside container**:
```bash
docker exec nanobot-user1 cat /root/.nanobot/config.json
```
### Environment Variables Not Applied
1. **Check env files are loaded**:
```bash
docker exec nanobot-user1 env | grep NANOBOT
```
2. **Recreate container** (env files loaded at creation):
```bash
docker compose -f docker-compose.multi.env.yml up -d --force-recreate nanobot-user1
```
### Port Already in Use
If port conflicts:
```bash
# Check what's using the port
sudo lsof -i :18790
# Stop conflicting container
docker stop <container_name>
# Or change port in docker-compose.multi.env.yml
```
---
## Reference
### Docker Compose Files
| File | Purpose | Use Case | Env Loading |
|------|---------|----------|-------------|
| `docker-compose.yml` | Single-bot baseline | One gateway + optional CLI, simplest setup | No `env_file`; uses mounted `~/.nanobot/config.json` and container environment |
| `docker-compose.multi.yml` | Multi-bot baseline | Multiple bots with separate config dirs, minimal env indirection | No `env_file`; each bot uses mounted `~/.nanobot-userX/config.json` and container environment |
| `docker-compose.multi.env.yml` | Multi-bot production | Stable multi-bot deployments with shared + per-bot overrides | Loads `.env.shared` first, then `.env.userX` (later file overrides earlier) |
| `docker-compose.multi.dev.yml` | Multi-bot development | Active code development with source mounted into containers | Same env behavior as `multi.env` (`.env.shared` + `.env.userX`) |
### Container Names
- `nanobot-user1` - Bot 1 container (production)
- `nanobot-user1-dev` - Bot 1 container (development)
- `nanobot-user2` - Bot 2 container
- `nanobot-user3` - Bot 3 container
### Ports
- User 1: `18790` (host) → `18790` (container)
- User 2: `18791` (host) → `18790` (container)
- User 3: `18792` (host) → `18790` (container)
### Config Locations
| Location | Used By | Purpose |
|----------|---------|---------|
| `~/.nanobot/config.json` | Host `nanobot gateway` | Original config (not used by Docker) |
| `~/.nanobot-user1/config.json` | Docker user1 | Bot 1 config (mounted into container) |
| `~/.nanobot-user2/config.json` | Docker user2 | Bot 2 config |
| `~/.nanobot-user3/config.json` | Docker user3 | Bot 3 config |
### Environment Files
| File | Loaded By | Purpose |
|------|-----------|---------|
| `.env.shared` | All Docker containers | Shared settings (API keys, model, etc.) |
| `.env.user1` | Docker user1 only | Bot 1 specific overrides |
| `.env.user2` | Docker user2 only | Bot 2 specific overrides |
| `.env.user3` | Docker user3 only | Bot 3 specific overrides |
| `.env` | Host `nanobot gateway` | Host environment (not used by Docker) |
### Quick Command Reference
```bash
# === START ===
docker compose -f docker-compose.multi.env.yml up -d nanobot-user1 # Start user1
docker compose -f docker-compose.multi.env.yml up -d # Start all
# === STOP ===
docker compose -f docker-compose.multi.env.yml stop nanobot-user1 # Stop user1
docker compose -f docker-compose.multi.env.yml down # Stop all
# === RESTART ===
docker compose -f docker-compose.multi.env.yml restart nanobot-user1 # Restart user1
docker compose -f docker-compose.multi.env.yml restart # Restart all
# === LOGS ===
docker logs -f nanobot-user1 # View logs
docker compose -f docker-compose.multi.env.yml logs -f nanobot-user1 # View logs
# === STATUS ===
docker compose -f docker-compose.multi.env.yml ps # List containers
docker ps | grep nanobot # List containers
# === REBUILD ===
docker compose -f docker-compose.multi.env.yml build nanobot-user1 # Rebuild user1
docker compose -f docker-compose.multi.env.yml build # Rebuild all
# === RECREATE (after config changes) ===
docker compose -f docker-compose.multi.env.yml up -d --force-recreate nanobot-user1
```
---
## Common Tasks
### Add a New Bot (User4)
1. Create config directory:
```bash
mkdir -p ~/.nanobot-user4
```
2. Create config file:
```bash
cat > ~/.nanobot-user4/config.json << 'EOF'
{
"channels": {
"telegram": {
"enabled": true,
"allowFrom": ["USERNAME"]
}
}
}
EOF
```
3. Create env file:
```bash
cp .env.user1 .env.user4
nano .env.user4 # Edit with bot-specific settings
```
4. Add to `docker-compose.multi.env.yml`:
```yaml
nanobot-user4:
# ... (copy from nanobot-user1, change port to 18793)
```
5. Start:
```bash
docker compose -f docker-compose.multi.env.yml up -d nanobot-user4
```
### Update Ollama API Base
If Ollama IP changes:
```bash
# Edit .env.shared
sed -i 's|http://172.17.0.1:11434|http://NEW_IP:11434|g' .env.shared
# Restart all containers
docker compose -f docker-compose.multi.env.yml restart
```
### Backup Configurations
```bash
# Backup all configs
tar -czf nanobot-configs-backup-$(date +%Y%m%d).tar.gz \
~/.nanobot-user* \
.env.shared .env.user*
# Restore
tar -xzf nanobot-configs-backup-YYYYMMDD.tar.gz
```
---
## Notes
- **Original config** (`~/.nanobot/config.json`) is NOT used by Docker containers
- **Host venv** is NOT needed to run Docker commands (Docker builds its own environment)
- **Environment variables** override config file settings
- **Arrays** (like `allowFrom`) must be in `config.json`, NOT in env files
- **Ollama API_BASE** must use `172.17.0.1` (Docker bridge) not `localhost`
---
## See Also
- `ENV_FILES_GUIDE.md` - Detailed env file management
- `DEVELOPMENT_WITH_DOCKER.md` - Development workflow details
- `SETUP_SUMMARY.md` - Initial setup summary
- `VERIFY_DOCKER_SETUP.md` - Verification checklist

241
ENV_FILES_GUIDE.md Normal file
View File

@ -0,0 +1,241 @@
# Using Separate Env Files Per Container
This guide shows you how to use separate `.env` files for each bot container, making it easy to manage both shared and bot-specific settings.
## How It Works
Docker Compose loads `env_file` entries in order. Later files override earlier ones:
1. **`.env.shared`** - Loaded first, contains common settings
2. **`.env.user1`, `.env.user2`, `.env.user3`** - Loaded after, can override shared settings
This means:
- ✅ Shared settings go in `.env.shared` (update once)
- ✅ Bot-specific overrides go in `.env.user1`, `.env.user2`, etc. (only if needed)
- ✅ Easy to edit (plain text files, no JSON)
## Quick Setup
### Step 1: Create Env Files
Run the setup script:
```bash
chmod +x env-files-setup.sh
./env-files-setup.sh
```
This creates:
- `.env.shared` - Shared settings for all bots
- `.env.user1` - Overrides for bot 1 (optional)
- `.env.user2` - Overrides for bot 2 (optional)
- `.env.user3` - Overrides for bot 3 (optional)
### Step 2: Edit `.env.shared`
Edit `.env.shared` with your shared settings:
```bash
# Shared configuration for all nanobot instances
NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-your-actual-key
NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5
NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.7
NANOBOT_AGENTS__DEFAULTS__MAX_TOKENS=8192
```
### Step 3: Edit Bot-Specific Files (Optional)
If a bot needs different settings, edit its `.env.userX` file:
**`.env.user1`** (example - override model for bot 1):
```bash
# Override model for this bot only
NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-sonnet-4
```
**`.env.user2`** (example - override temperature):
```bash
# Override temperature for this bot only
NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.9
```
**`.env.user3`** (example - leave empty to use all shared settings):
```bash
# This bot uses all settings from .env.shared
# No overrides needed
```
### Step 4: Create Minimal Config Files
Each bot still needs a minimal `config.json` with bot-specific channel settings:
**`~/.nanobot-user1/config.json`**:
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "BOT_TOKEN_FOR_USER1",
"allowFrom": ["USER1_TELEGRAM_ID"]
}
}
}
```
**`~/.nanobot-user2/config.json`**:
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "BOT_TOKEN_FOR_USER2",
"allowFrom": ["USER2_TELEGRAM_ID"]
}
}
}
```
### Step 5: Run with Docker Compose
```bash
docker compose -f docker-compose.multi.env.yml up -d
```
## File Structure
```
nanobot/
├── .env.shared ← Shared settings (API keys, model, etc.)
├── .env.user1 ← Bot 1 overrides (optional)
├── .env.user2 ← Bot 2 overrides (optional)
├── .env.user3 ← Bot 3 overrides (optional)
├── docker-compose.multi.env.yml
└── ~/.nanobot-user1/
└── config.json ← Bot 1 channel config (Telegram token, user ID)
└── ~/.nanobot-user2/
└── config.json ← Bot 2 channel config
└── ~/.nanobot-user3/
└── config.json ← Bot 3 channel config
```
## Examples
### Example 1: All Bots Use Same Settings
**`.env.shared`**:
```bash
NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-xxx
NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5
```
**`.env.user1`**, **`.env.user2`**, **`.env.user3`**:
```bash
# Empty - all bots use shared settings
```
### Example 2: One Bot Uses Different Model
**`.env.shared`**:
```bash
NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-xxx
NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5
```
**`.env.user1`**:
```bash
# Bot 1 uses a different model
NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-sonnet-4
```
**`.env.user2`**, **`.env.user3`**:
```bash
# Empty - use shared model
```
### Example 3: One Bot Uses Different API Key
**`.env.shared`**:
```bash
NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-shared-key
NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5
```
**`.env.user2`**:
```bash
# Bot 2 uses its own API key
NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-user2-key
```
## Updating Settings
### Update Shared Settings
Edit `.env.shared` and restart all containers:
```bash
# Edit shared settings
nano .env.shared
# Restart all bots
docker compose -f docker-compose.multi.env.yml restart
```
### Update Bot-Specific Settings
Edit the specific `.env.userX` file and restart that bot:
```bash
# Edit bot 1's settings
nano .env.user1
# Restart only bot 1
docker restart nanobot-user1
```
## Environment Variable Format
Nanobot uses Pydantic's `BaseSettings` with:
- Prefix: `NANOBOT_`
- Nested delimiter: `__` (double underscore)
Examples:
- `NANOBOT_PROVIDERS__OPENROUTER__API_KEY``providers.openrouter.apiKey`
- `NANOBOT_AGENTS__DEFAULTS__MODEL``agents.defaults.model`
- `NANOBOT_AGENTS__DEFAULTS__TEMPERATURE``agents.defaults.temperature`
## Advantages
**Easy to edit** - Plain text files, no JSON syntax
**Clear separation** - Shared vs bot-specific settings
**Flexible** - Override only what you need
**Version control friendly** - Can commit `.env.shared`, ignore `.env.userX` if they contain secrets
**No config.json editing** - Only edit env files for most changes
## Troubleshooting
### Settings Not Applied
1. Check if env files exist:
```bash
ls -la .env.*
```
2. Check what's loaded in container:
```bash
docker exec nanobot-user1 env | grep NANOBOT
```
3. Restart container after changes:
```bash
docker restart nanobot-user1
```
### Override Not Working
Remember: Later files override earlier ones. If `.env.user1` has a setting but it's not applied, check:
- Is the variable name correct? (use `__` not `.`)
- Did you restart the container?
- Is the setting in `.env.shared` overriding it? (remove from `.env.shared` if you want bot-specific only)

View File

@ -0,0 +1,222 @@
# Managing Multiple Bot Configs Efficiently
Yes, with Docker you'll need to manage multiple config files, but here are strategies to make it easier:
## The Challenge
When you have 3 bots, you have 3 config files:
- `~/.nanobot-user1/config.json`
- `~/.nanobot-user2/config.json`
- `~/.nanobot-user3/config.json`
If you need to change something like:
- API key (shared across all bots)
- Model settings (might be shared)
- Tool configurations (might be shared)
You'd normally need to edit all 3 files.
## Solution 1: Use Environment Variables for Shared Settings
Nanobot supports environment variables! You can set shared settings via environment variables in Docker.
### Update docker-compose.multi.yml
```yaml
services:
nanobot-user1:
# ... existing config ...
environment:
# Shared settings - set once, applies to all
NANOBOT_PROVIDERS__OPENROUTER__API_KEY: "sk-or-v1-xxx"
NANOBOT_AGENTS__DEFAULTS__MODEL: "anthropic/claude-opus-4-5"
NANOBOT_AGENTS__DEFAULTS__TEMPERATURE: "0.7"
# Bot-specific settings still in config.json
volumes:
- ~/.nanobot-user1:/root/.nanobot
nanobot-user2:
# ... same environment variables ...
environment:
NANOBOT_PROVIDERS__OPENROUTER__API_KEY: "sk-or-v1-xxx"
NANOBOT_AGENTS__DEFAULTS__MODEL: "anthropic/claude-opus-4-5"
NANOBOT_AGENTS__DEFAULTS__TEMPERATURE: "0.7"
volumes:
- ~/.nanobot-user2:/root/.nanobot
```
**Better yet**, use a shared `.env` file:
### Create `.env.shared`:
```bash
# Shared settings for all bots
NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-xxx
NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5
NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.7
NANOBOT_AGENTS__DEFAULTS__MAX_TOKENS=8192
```
### Update docker-compose.multi.yml:
```yaml
services:
nanobot-user1:
env_file:
- .env.shared # Load shared settings
volumes:
- ~/.nanobot-user1:/root/.nanobot
# ... rest of config ...
nanobot-user2:
env_file:
- .env.shared # Same shared settings
volumes:
- ~/.nanobot-user2:/root/.nanobot
# ... rest of config ...
```
Now you only update `.env.shared` for shared settings!
## Solution 2: Minimal Configs + Environment Variables
Keep only bot-specific settings in config files:
**`~/.nanobot-user1/config.json`** (minimal):
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "BOT_TOKEN_1",
"allowFrom": ["USER_ID_1"]
}
}
}
```
**`~/.nanobot-user2/config.json`** (minimal):
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "BOT_TOKEN_2",
"allowFrom": ["USER_ID_2"]
}
}
}
```
Everything else comes from `.env.shared`!
## Solution 3: Use the Update Script
I've created `update-multi-configs.sh` to batch-update configs:
```bash
# Update API key in all configs
./update-multi-configs.sh update-api-key openrouter "sk-or-v1-new-key"
# Update model in all configs
./update-multi-configs.sh update-model "anthropic/claude-opus-4-5"
# Update any setting
./update-multi-configs.sh update-setting "agents.defaults.temperature" "0.8"
```
Requires `jq` to be installed:
```bash
sudo apt install jq # Linux
brew install jq # macOS
```
## Solution 4: Base Config Template
Create a template and only override what's different:
**`~/.nanobot-base/config.json`** (template):
```json
{
"providers": {
"openrouter": {
"apiKey": "sk-or-v1-xxx"
}
},
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-5",
"temperature": 0.7
}
},
"channels": {
"telegram": {
"enabled": true,
"token": "REPLACE_WITH_BOT_TOKEN",
"allowFrom": ["REPLACE_WITH_USER_ID"]
}
}
}
```
Then create bot-specific configs by copying and modifying:
```bash
cp ~/.nanobot-base/config.json ~/.nanobot-user1/config.json
# Edit only the telegram token and user ID
cp ~/.nanobot-base/config.json ~/.nanobot-user2/config.json
# Edit only the telegram token and user ID
```
## Recommended Approach
**Use Solution 1 (Environment Variables)** - it's the cleanest:
1. Create `.env.shared` with all shared settings
2. Keep minimal configs with only bot-specific settings (Telegram tokens/user IDs)
3. Update `.env.shared` when you need to change shared settings
4. Restart containers to apply changes
This way:
- ✅ Shared settings: Update once in `.env.shared`
- ✅ Bot-specific settings: Only in each config.json
- ✅ Easy to manage and maintain
## Example: Complete Setup
**`.env.shared`**:
```bash
NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-xxx
NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5
NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.7
NANOBOT_AGENTS__DEFAULTS__MAX_TOKENS=8192
```
**`~/.nanobot-user1/config.json`**:
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "1234567890:ABC...",
"allowFrom": ["123456789"]
}
}
}
```
**`docker-compose.multi.yml`**:
```yaml
services:
nanobot-user1:
env_file:
- .env.shared
volumes:
- ~/.nanobot-user1:/root/.nanobot
# ... rest ...
```
Now when you need to change the API key or model, just edit `.env.shared` and restart!

261
MULTI_BOT_SETUP.md Normal file
View File

@ -0,0 +1,261 @@
# Running Multiple Nanobot Gateways with Docker
This guide shows you how to run multiple nanobot gateway instances, each with its own Telegram bot.
## Quick Start
### Step 1: Setup Config Directories
Run the setup script:
```bash
./multi-bot-setup.sh
```
Or manually:
```bash
# Create directories
mkdir -p ~/.nanobot-user1
mkdir -p ~/.nanobot-user2
mkdir -p ~/.nanobot-user3
# Copy your base config (if you have one)
cp ~/.nanobot/config.json ~/.nanobot-user1/config.json
cp ~/.nanobot/config.json ~/.nanobot-user2/config.json
cp ~/.nanobot/config.json ~/.nanobot-user3/config.json
```
### Step 2: Configure Each Bot
Edit each config file with different Telegram bot tokens:
**`~/.nanobot-user1/config.json`:**
```json
{
"providers": {
"openrouter": {
"apiKey": "sk-or-v1-xxx"
}
},
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-5"
}
},
"channels": {
"telegram": {
"enabled": true,
"token": "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz",
"allowFrom": ["123456789"]
}
}
}
```
**`~/.nanobot-user2/config.json`:**
```json
{
"providers": {
"openrouter": {
"apiKey": "sk-or-v1-xxx"
}
},
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-5"
}
},
"channels": {
"telegram": {
"enabled": true,
"token": "9876543210:XYZabcDEFghiJKLmnopQRSuvwx",
"allowFrom": ["987654321"]
}
}
}
```
**`~/.nanobot-user3/config.json`:**
```json
{
"providers": {
"openrouter": {
"apiKey": "sk-or-v1-xxx"
}
},
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-5"
}
},
"channels": {
"telegram": {
"enabled": true,
"token": "5551234567:LMNopqRSTuvwXYZabcdefGHIjkl",
"allowFrom": ["555123456"]
}
}
}
```
### Step 3: Run with Docker Compose (Recommended)
```bash
# Build the image (first time only)
docker compose -f docker-compose.multi.yml build
# Start all bots
docker compose -f docker-compose.multi.yml up -d
# View logs
docker compose -f docker-compose.multi.yml logs -f
# Stop all bots
docker compose -f docker-compose.multi.yml down
# Start/stop individual bots
docker compose -f docker-compose.multi.yml start nanobot-user1
docker compose -f docker-compose.multi.yml stop nanobot-user2
```
### Step 4: Run with Docker Run Commands (Alternative)
If you prefer `docker run` commands:
```bash
# Build the image first
docker build -t nanobot .
# Run bot 1
docker run -d \
--name nanobot-user1 \
-v ~/.nanobot-user1:/root/.nanobot \
-p 18790:18790 \
--restart unless-stopped \
nanobot gateway
# Run bot 2
docker run -d \
--name nanobot-user2 \
-v ~/.nanobot-user2:/root/.nanobot \
-p 18791:18790 \
--restart unless-stopped \
nanobot gateway
# Run bot 3
docker run -d \
--name nanobot-user3 \
-v ~/.nanobot-user3:/root/.nanobot \
-p 18792:18790 \
--restart unless-stopped \
nanobot gateway
```
## Managing Containers
### View Logs
```bash
# All bots
docker compose -f docker-compose.multi.yml logs -f
# Specific bot
docker logs -f nanobot-user1
# Or with docker-compose
docker compose -f docker-compose.multi.yml logs -f nanobot-user1
```
### Stop/Start Containers
```bash
# Stop all
docker compose -f docker-compose.multi.yml down
# Start all
docker compose -f docker-compose.multi.yml up -d
# Restart specific bot
docker restart nanobot-user1
# Stop specific bot
docker stop nanobot-user1
docker start nanobot-user1
```
### Check Status
```bash
# List all running containers
docker ps | grep nanobot
# Check logs for errors
docker logs nanobot-user1 --tail 50
```
## Port Mapping
Each bot uses a different host port:
- **User 1**: Port `18790` → Container port `18790`
- **User 2**: Port `18791` → Container port `18790`
- **User 3**: Port `18792` → Container port `18790`
The gateway port inside the container is always `18790`, but mapped to different host ports to avoid conflicts.
## Creating Telegram Bots
For each user, create a separate bot:
1. Open Telegram, search `@BotFather`
2. Send `/newbot`
3. Follow prompts to create a bot
4. Copy the token
5. Add it to the respective config file
## Troubleshooting
### Bot not responding
```bash
# Check if container is running
docker ps | grep nanobot-user1
# Check logs for errors
docker logs nanobot-user1
# Verify config is correct
cat ~/.nanobot-user1/config.json | jq '.channels.telegram'
```
### Port already in use
If you get port conflicts, change the port mappings in `docker-compose.multi.yml`:
```yaml
ports:
- "18890:18790" # Change 18790 to 18890
```
### Config not loading
Make sure the volume mount is correct:
```bash
# Verify config exists
ls -la ~/.nanobot-user1/config.json
# Check if it's readable
docker exec nanobot-user1 cat /root/.nanobot/config.json
```
## Adding More Bots
To add more bots:
1. Create new directory: `mkdir -p ~/.nanobot-user4`
2. Copy config: `cp ~/.nanobot-user1/config.json ~/.nanobot-user4/config.json`
3. Edit config with new bot token
4. Add new service to `docker-compose.multi.yml` (copy existing service, change name and port)
5. Run: `docker compose -f docker-compose.multi.yml up -d nanobot-user4`

106
QUICK_REFERENCE.md Normal file
View File

@ -0,0 +1,106 @@
# Quick Reference Card
## 🚀 Most Common Commands
### Start/Stop
```bash
# Start user1 only
docker compose -f docker-compose.multi.env.yml up -d nanobot-user1
# Start all bots
docker compose -f docker-compose.multi.env.yml up -d
# Stop user1 only
docker compose -f docker-compose.multi.env.yml stop nanobot-user1
# Stop all bots
docker compose -f docker-compose.multi.env.yml down
# Restart user1
docker compose -f docker-compose.multi.env.yml restart nanobot-user1
```
### Logs
```bash
# View logs (follow)
docker logs -f nanobot-user1
# View last 50 lines
docker logs --tail 50 nanobot-user1
```
### Status
```bash
# Check what's running
docker compose -f docker-compose.multi.env.yml ps
# Check specific container
docker ps | grep nanobot-user1
```
### Configuration
```bash
# Edit shared settings
nano .env.shared
docker compose -f docker-compose.multi.env.yml restart
# Edit bot-specific settings
nano .env.user1
docker restart nanobot-user1
# Edit config file
nano ~/.nanobot-user1/config.json
docker restart nanobot-user1
```
### Development
```bash
# Use dev mode (mounts source code)
docker compose -f docker-compose.multi.dev.yml up -d nanobot-user1
# After code changes
docker compose -f docker-compose.multi.dev.yml restart nanobot-user1
```
## 📁 File Locations
| What | Where |
|------|-------|
| Shared settings | `.env.shared` |
| Bot 1 settings | `.env.user1` |
| Bot 1 config | `~/.nanobot-user1/config.json` |
| Production compose | `docker-compose.multi.env.yml` |
| Dev compose | `docker-compose.multi.dev.yml` |
## 🧭 Which Compose File?
| File | Best For | Uses env files? |
|------|----------|-----------------|
| `docker-compose.yml` | Single bot (`nanobot-gateway` + optional `nanobot-cli`) | No |
| `docker-compose.multi.yml` | Multi-bot with per-user config directories | No |
| `docker-compose.multi.env.yml` | Multi-bot with shared + per-user env overrides (recommended) | Yes: `.env.shared` then `.env.userX` |
| `docker-compose.multi.dev.yml` | Same as above, but with source code mounted for development | Yes: `.env.shared` then `.env.userX` |
## 🔧 Troubleshooting
```bash
# Bot not responding?
docker logs nanobot-user1 --tail 50
# Connection error?
grep API_BASE .env.shared # Should be 172.17.0.1:11434
# Config not loading?
docker exec nanobot-user1 cat /root/.nanobot/config.json
```
## 📖 Full Documentation
See `DOCKER_MULTI_BOT_GUIDE.md` for complete guide.

View File

@ -742,6 +742,8 @@ That's it! Environment variables, model prefixing, config matching, and `nanobot
nanobot supports [MCP](https://modelcontextprotocol.io/) — connect external tool servers and use them as native agent tools. nanobot supports [MCP](https://modelcontextprotocol.io/) — connect external tool servers and use them as native agent tools.
For a full Gmail MCP walkthrough (config + OAuth + verification), see [`docs/gmail_mcp_setup.md`](docs/gmail_mcp_setup.md).
Add MCP servers to your `config.json`: Add MCP servers to your `config.json`:
```json ```json
@ -827,6 +829,15 @@ vim ~/.nanobot/config.json # add API keys
docker compose up -d nanobot-gateway # start gateway docker compose up -d nanobot-gateway # start gateway
``` ```
#### Which Compose File To Use?
| File | Scenario | Notes |
|------|----------|-------|
| `docker-compose.yml` | Single-bot local usage | One gateway + optional CLI, mounts `~/.nanobot` |
| `docker-compose.multi.yml` | Multi-bot with separate per-user config directories | No `env_file`; use `~/.nanobot-userX/config.json` per bot |
| `docker-compose.multi.env.yml` | Multi-bot with shared and per-user environment overrides | Loads `.env.shared` then `.env.userX` (recommended for multi-bot ops) |
| `docker-compose.multi.dev.yml` | Multi-bot development | Same env layering as `multi.env`, plus source mount for live code iteration |
```bash ```bash
docker compose run --rm nanobot-cli agent -m "Hello!" # run CLI docker compose run --rm nanobot-cli agent -m "Hello!" # run CLI
docker compose logs -f nanobot-gateway # view logs docker compose logs -f nanobot-gateway # view logs

116
SETUP_SUMMARY.md Normal file
View File

@ -0,0 +1,116 @@
# Multi-Bot Docker Setup Summary
## ✅ Setup Complete
### Files Created
1. **Environment Files:**
- `.env.shared` - Shared settings (providers, agents, tools, gateway)
- `.env.user1` - Bot 1 specific settings (Telegram token, email credentials)
- `.env.user2` - Bot 2 specific settings (placeholder)
- `.env.user3` - Bot 3 specific settings (placeholder)
2. **Config Files:**
- `~/.nanobot-user1/config.json` - Bot 1 channel config (allowFrom arrays)
- `~/.nanobot-user2/config.json` - Bot 2 channel config (placeholder)
- `~/.nanobot-user3/config.json` - Bot 3 channel config (placeholder)
3. **Docker Compose:**
- `docker-compose.multi.env.yml` - Multi-bot Docker Compose configuration
## 🔍 How It Works
### Configuration Loading Order
1. **Docker Compose loads environment files:**
- First: `.env.shared` (shared settings)
- Second: `.env.user1` (bot-specific overrides)
- Later files override earlier ones ✅
2. **Container starts with volume mount:**
- Host: `~/.nanobot-user1`
- Container: `/root/.nanobot`
- This maps your config directory into the container ✅
3. **Nanobot loads configuration:**
- **Environment variables**: Automatically loaded by Pydantic `BaseSettings`
- Format: `NANOBOT_CHANNELS__TELEGRAM__TOKEN=...`
- These come from Docker environment (loaded from `.env.shared` + `.env.user1`)
- **Config file**: Loaded from `/root/.nanobot/config.json`
- Inside container: `/root/.nanobot/config.json`
- On host: `~/.nanobot-user1/config.json` (mounted)
- Contains: `allowFrom` arrays (can't be in env vars)
### Important: Original Config is NOT Used
**Your original config** (`~/.nanobot/config.json`) is **NOT** used by Docker containers.
Each container uses:
- Its own env files (`.env.shared` + `.env.userX`)
- Its own config directory (`~/.nanobot-userX`)
This means:
- ✅ Original config stays untouched
- ✅ Each bot has isolated configuration
- ✅ No conflicts between bots
## 📋 Current Configuration
### Bot 1 (nanobot-user1)
**Environment Variables** (from `.env.shared` + `.env.user1`):
- Provider: Custom/Ollama (`http://localhost:11434/v1`)
- Model: `llama3.1:8b`
- Workspace: `/mnt/data/nanobot`
- Telegram: Enabled with token
- Email: Enabled with credentials
**Config File** (`~/.nanobot-user1/config.json`):
- Telegram `allowFrom`: `["TADec2023"]`
- Email `allowFrom`: `["adayear2025@gmail.com"]`
## 🚀 Running the Bots
```bash
# Build Docker image (first time)
docker compose -f docker-compose.multi.env.yml build
# Start all bots
docker compose -f docker-compose.multi.env.yml up -d
# View logs
docker compose -f docker-compose.multi.env.yml logs -f
# Stop all bots
docker compose -f docker-compose.multi.env.yml down
# Restart specific bot
docker restart nanobot-user1
```
## ✅ Verification Checklist
- [x] `.env.shared` created with shared settings
- [x] `.env.user1` created with bot-specific settings
- [x] `ALLOW_FROM` removed from env files (arrays belong in config.json)
- [x] Config directories created (`~/.nanobot-user1`, etc.)
- [x] Config files created with `allowFrom` arrays
- [x] Docker Compose file configured correctly
- [x] Volume mounts map host configs to container paths
## 🎯 Key Points
1. **Environment Variables** → For simple key-value settings (tokens, API keys, models)
2. **Config Files** → For complex settings (arrays like `allowFrom`)
3. **Docker Mounts** → Each container gets its own config directory
4. **Original Config** → Not used by Docker containers (stays safe)
## 📝 Next Steps
1. Update `.env.user2` and `.env.user3` with bot-specific settings
2. Update `~/.nanobot-user2/config.json` and `~/.nanobot-user3/config.json` with actual user IDs/emails
3. Run `docker compose -f docker-compose.multi.env.yml up -d` to start all bots
4. Check logs to verify each bot is using correct configuration

125
VERIFY_DOCKER_SETUP.md Normal file
View File

@ -0,0 +1,125 @@
# Verifying Docker Multi-Bot Setup
## ✅ Configuration Check
### 1. Environment Files
**`.env.shared`** - Contains:
- ✅ Provider settings (custom/Ollama)
- ✅ Agent defaults (model, workspace, temperature, etc.)
- ✅ Tool settings
- ✅ Gateway settings
- ✅ Email IMAP/SMTP settings (shared)
**`.env.user1`** - Contains:
- ✅ Telegram token (bot-specific)
- ✅ Email credentials (bot-specific override)
- ⚠️ **Issue**: `ALLOW_FROM` should NOT be in env files (arrays don't work in env vars)
### 2. Config Files
**`~/.nanobot-user1/config.json`** - Contains:
- ✅ Telegram `allowFrom` array (correct location)
- ✅ Email `allowFrom` array (correct location)
### 3. Docker Compose
**`docker-compose.multi.env.yml`** - Correctly configured:
- ✅ Loads `.env.shared` first
- ✅ Loads `.env.user1` second (overrides shared)
- ✅ Mounts `~/.nanobot-user1:/root/.nanobot` (maps host config to container)
## 🔍 How Nanobot Loads Config in Docker
1. **Environment Variables** (from `.env.shared` and `.env.user1`):
- Loaded by Docker Compose into container environment
- Nanobot's Pydantic `BaseSettings` reads them automatically
- Format: `NANOBOT_CHANNELS__TELEGRAM__TOKEN=...`
2. **Config File** (`config.json`):
- Path inside container: `/root/.nanobot/config.json`
- Maps to host: `~/.nanobot-user1/config.json`
- Loaded by `load_config()` which calls `get_config_path()`
- `get_config_path()` returns `Path.home() / ".nanobot" / "config.json"`
- In Docker, `Path.home()` = `/root`, so it reads `/root/.nanobot/config.json`
- This is mounted from `~/.nanobot-user1/config.json` on host ✅
## ⚠️ Issues Found
### Issue 1: ALLOW_FROM in env file
**Problem**: `.env.user1` has:
```bash
NANOBOT_CHANNELS__TELEGRAM__ALLOW_FROM=["TADec2023"]
NANOBOT_CHANNELS__EMAIL__ALLOW_FROM=["adayear2025@gmail.com"]
```
**Why it's wrong**: Environment variables can't handle JSON arrays. These will be treated as strings, not arrays.
**Fix**: Remove these from `.env.user1` - they're already correctly in `config.json`:
```json
{
"channels": {
"telegram": {
"allowFrom": ["TADec2023"]
},
"email": {
"allowFrom": ["adayear2025@gmail.com"]
}
}
}
```
### Issue 2: Duplicate email settings
**Problem**: Email IMAP/SMTP settings are in both `.env.shared` and `.env.user1`
**Recommendation**:
- If all bots use same email account → Keep only in `.env.shared`
- If each bot uses different email → Keep only in `.env.userX` files
## ✅ Verification Steps
1. **Check env files don't have ALLOW_FROM**:
```bash
grep ALLOW_FROM .env.user1
# Should return nothing or be removed
```
2. **Check config files have allowFrom**:
```bash
cat ~/.nanobot-user1/config.json | jq '.channels.telegram.allowFrom'
# Should show: ["TADec2023"]
```
3. **Verify Docker mounts**:
```bash
docker compose -f docker-compose.multi.env.yml config | grep -A 5 "nanobot-user1"
# Should show volume mount: ~/.nanobot-user1:/root/.nanobot
```
4. **Test config loading in container**:
```bash
docker run --rm -v ~/.nanobot-user1:/root/.nanobot \
-e NANOBOT_CHANNELS__TELEGRAM__TOKEN=test \
nanobot gateway --help
```
## 🎯 Summary
**What's Correct:**
- ✅ Docker Compose loads env files correctly
- ✅ Config files are in correct locations
- ✅ Volume mounts map host configs to container paths
- ✅ Nanobot will read from `/root/.nanobot/config.json` inside container
**What Needs Fixing:**
- ⚠️ Remove `ALLOW_FROM` from `.env.user1` (keep only in config.json)
- ⚠️ Decide: Email settings in `.env.shared` OR `.env.userX` (not both)
**How It Works:**
1. Docker Compose loads `.env.shared` → sets environment variables
2. Docker Compose loads `.env.user1` → overrides with bot-specific vars
3. Container starts → mounts `~/.nanobot-user1` to `/root/.nanobot`
4. Nanobot starts → reads env vars (from Docker) + config.json (from mount)
5. Result: Bot uses combined settings from env vars + config.json ✅

65
create-bot-configs.sh Executable file
View File

@ -0,0 +1,65 @@
#!/bin/bash
# Create minimal config.json files for each bot
# These only contain channel-specific settings that can't be in env vars
set -e
echo "Creating bot config files..."
# Bot 1 config
cat > ~/.nanobot-user1/config.json << 'EOF'
{
"channels": {
"telegram": {
"enabled": true,
"allowFrom": ["TADec2023"]
},
"email": {
"enabled": true,
"allowFrom": ["adayear2025@gmail.com"]
}
}
}
EOF
# Bot 2 config (placeholder - update with actual values)
cat > ~/.nanobot-user2/config.json << 'EOF'
{
"channels": {
"telegram": {
"enabled": true,
"allowFrom": []
},
"email": {
"enabled": true,
"allowFrom": []
}
}
}
EOF
# Bot 3 config (placeholder - update with actual values)
cat > ~/.nanobot-user3/config.json << 'EOF'
{
"channels": {
"telegram": {
"enabled": true,
"allowFrom": []
},
"email": {
"enabled": true,
"allowFrom": []
}
}
}
EOF
echo "✓ Created config files:"
echo " - ~/.nanobot-user1/config.json"
echo " - ~/.nanobot-user2/config.json"
echo " - ~/.nanobot-user3/config.json"
echo ""
echo "Note: Telegram tokens come from .env.userX files"
echo " Update allowFrom arrays with actual user IDs/emails"

View File

@ -0,0 +1,80 @@
# Development version - mounts source code for live updates
# Use this when developing nanobot code
# Changes to nanobot/ directory will be picked up automatically (may need container restart)
services:
nanobot-user1:
build:
context: .
dockerfile: Dockerfile
container_name: nanobot-user1-dev
command: ["gateway"]
restart: unless-stopped
env_file:
- .env.shared
- .env.user1
volumes:
- ~/.nanobot-user1:/root/.nanobot
# Mount source code for development (changes picked up immediately)
- ./nanobot:/app/nanobot:ro # Read-only mount (safer)
# Or use this for read-write (if you edit inside container):
# - ./nanobot:/app/nanobot
ports:
- "18790:18790"
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M
nanobot-user2:
build:
context: .
dockerfile: Dockerfile
container_name: nanobot-user2-dev
command: ["gateway"]
restart: unless-stopped
env_file:
- .env.shared
- .env.user2
volumes:
- ~/.nanobot-user2:/root/.nanobot
- ./nanobot:/app/nanobot:ro
ports:
- "18791:18790"
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M
nanobot-user3:
build:
context: .
dockerfile: Dockerfile
container_name: nanobot-user3-dev
command: ["gateway"]
restart: unless-stopped
env_file:
- .env.shared
- .env.user3
volumes:
- ~/.nanobot-user3:/root/.nanobot
- ./nanobot:/app/nanobot:ro
ports:
- "18792:18790"
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M

View File

@ -0,0 +1,75 @@
# Using separate env files per container:
# - .env.shared: Common settings (API keys, model, etc.) - loaded first
# - .env.user1, .env.user2, .env.user3: Bot-specific overrides - loaded after
# Later files override earlier ones, so bot-specific settings take precedence
services:
nanobot-user1:
build:
context: .
dockerfile: Dockerfile
container_name: nanobot-user1
command: ["gateway"]
restart: unless-stopped
env_file:
- .env.shared # Shared settings (loaded first)
- .env.user1 # Bot-specific overrides (loaded second, overrides shared)
volumes:
- ~/.nanobot-user1:/root/.nanobot
ports:
- "18790:18790"
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M
nanobot-user2:
build:
context: .
dockerfile: Dockerfile
container_name: nanobot-user2
command: ["gateway"]
restart: unless-stopped
env_file:
- .env.shared # Shared settings (loaded first)
- .env.user2 # Bot-specific overrides (loaded second, overrides shared)
volumes:
- ~/.nanobot-user2:/root/.nanobot
ports:
- "18791:18790"
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M
nanobot-user3:
build:
context: .
dockerfile: Dockerfile
container_name: nanobot-user3
command: ["gateway"]
restart: unless-stopped
env_file:
- .env.shared # Shared settings (loaded first)
- .env.user3 # Bot-specific overrides (loaded second, overrides shared)
volumes:
- ~/.nanobot-user3:/root/.nanobot
ports:
- "18792:18790"
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M

61
docker-compose.multi.yml Normal file
View File

@ -0,0 +1,61 @@
services:
nanobot-user1:
build:
context: .
dockerfile: Dockerfile
container_name: nanobot-user1
command: ["gateway"]
restart: unless-stopped
volumes:
- ~/.nanobot-user1:/root/.nanobot
ports:
- "18790:18790"
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M
nanobot-user2:
build:
context: .
dockerfile: Dockerfile
container_name: nanobot-user2
command: ["gateway"]
restart: unless-stopped
volumes:
- ~/.nanobot-user2:/root/.nanobot
ports:
- "18791:18790"
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M
nanobot-user3:
build:
context: .
dockerfile: Dockerfile
container_name: nanobot-user3
command: ["gateway"]
restart: unless-stopped
volumes:
- ~/.nanobot-user3:/root/.nanobot
ports:
- "18792:18790"
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M

70
env-files-setup.sh Executable file
View File

@ -0,0 +1,70 @@
#!/bin/bash
# Setup script to create env files for multi-bot setup
set -e
echo "Setting up environment files for multi-bot configuration..."
echo ""
# Create .env.shared if it doesn't exist
if [ ! -f .env.shared ]; then
cat > .env.shared << 'EOF'
# Shared configuration for all nanobot instances
# These settings apply to all bots unless overridden in .env.user1, .env.user2, etc.
# LLM Provider API Key (shared across all bots)
NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-xxx
# Default Model (shared across all bots)
NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-opus-4-5
# Agent Settings (shared)
NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.7
NANOBOT_AGENTS__DEFAULTS__MAX_TOKENS=8192
NANOBOT_AGENTS__DEFAULTS__MAX_TOOL_ITERATIONS=20
NANOBOT_AGENTS__DEFAULTS__MEMORY_WINDOW=50
# Tool Settings (shared)
NANOBOT_TOOLS__RESTRICT_TO_WORKSPACE=true
# Gateway Settings (shared)
NANOBOT_GATEWAY__PORT=18790
NANOBOT_GATEWAY__HOST=0.0.0.0
EOF
echo "✓ Created .env.shared"
else
echo "⚠ .env.shared already exists, skipping..."
fi
# Create bot-specific env files
for i in 1 2 3; do
env_file=".env.user${i}"
if [ ! -f "$env_file" ]; then
cat > "$env_file" << EOF
# Bot-specific configuration for user${i}
# These settings override .env.shared for this bot only
# Leave empty or comment out to use shared settings
# Example: Override model for this bot
# NANOBOT_AGENTS__DEFAULTS__MODEL=anthropic/claude-sonnet-4
# Example: Override temperature for this bot
# NANOBOT_AGENTS__DEFAULTS__TEMPERATURE=0.9
# Example: Use different API key for this bot
# NANOBOT_PROVIDERS__OPENROUTER__API_KEY=sk-or-v1-different-key
EOF
echo "✓ Created $env_file"
else
echo "$env_file already exists, skipping..."
fi
done
echo ""
echo "Done! Next steps:"
echo "1. Edit .env.shared and add your API keys and shared settings"
echo "2. Edit .env.user1, .env.user2, .env.user3 if you need bot-specific overrides"
echo "3. Create minimal config.json files in ~/.nanobot-user1, ~/.nanobot-user2, etc."
echo "4. Run: docker compose -f docker-compose.multi.env.yml up -d"

49
multi-bot-setup.sh Executable file
View File

@ -0,0 +1,49 @@
#!/bin/bash
# Setup script for multiple nanobot instances
# Create directories for each bot
mkdir -p ~/.nanobot-user1
mkdir -p ~/.nanobot-user2
mkdir -p ~/.nanobot-user3
# Copy base config if it exists
if [ -f ~/.nanobot/config.json ]; then
cp ~/.nanobot/config.json ~/.nanobot-user1/config.json
cp ~/.nanobot/config.json ~/.nanobot-user2/config.json
cp ~/.nanobot/config.json ~/.nanobot-user3/config.json
echo "✓ Copied base config to all directories"
else
echo "⚠ Base config not found. Creating minimal configs..."
# Create minimal configs
cat > ~/.nanobot-user1/config.json <<EOF
{
"providers": {
"openrouter": {
"apiKey": "YOUR_API_KEY_HERE"
}
},
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-5"
}
},
"channels": {
"telegram": {
"enabled": true,
"token": "BOT_TOKEN_FOR_USER1",
"allowFrom": ["USER1_TELEGRAM_ID"]
}
}
}
EOF
cp ~/.nanobot-user1/config.json ~/.nanobot-user2/config.json
cp ~/.nanobot-user1/config.json ~/.nanobot-user3/config.json
fi
echo ""
echo "Next steps:"
echo "1. Edit each config file (~/.nanobot-user1/config.json, etc.)"
echo "2. Add different Telegram bot tokens and user IDs"
echo "3. Run the Docker containers (see docker-compose.multi.yml)"

View File

@ -120,7 +120,11 @@ Always be helpful, accurate, and concise. Before calling tools, briefly tell the
When remembering something important, write to {workspace_path}/memory/MEMORY.md When remembering something important, write to {workspace_path}/memory/MEMORY.md
To recall past events, grep {workspace_path}/memory/HISTORY.md To recall past events, grep {workspace_path}/memory/HISTORY.md
IMPORTANT: For email queries (latest email, email sender, inbox, etc.), ALWAYS use the read_emails tool. NEVER use exec() with mail/tail/awk commands or read_file() on /var/mail - those will not work. The read_emails tool is the only way to access emails.""" IMPORTANT: For owner-initiated email queries about the mailbox itself (e.g. "what's my latest email?", "list unread emails", "search my inbox"), prefer the read_emails tool over any other method. NEVER use exec() with mail/tail/awk commands or read_file() on /var/mail - those will not work. The read_emails tool is the only way to access the mailbox via IMAP.
When you are replying on the email channel to an external sender, treat the incoming "Email received. From: ... Subject: ... Date: ...\\n\\n<body>" content as their message and write a direct, helpful reply. Do NOT call read_emails just because the message is an email; only call read_emails if the human explicitly asks you to inspect or search the mailbox (e.g. "check my inbox", "find an email", etc.).
**Tool failures:** If a tool result says authentication failed, API access failed, or includes a tag like [NO_CALENDAR_DATA], you have no real data from that integrationnever fabricate meetings, mailbox contents, or file contents. Say access failed and what to fix. For other tool errors (e.g. missing parameters), correct the call or explain the error without inventing facts."""
def _load_bootstrap_files(self) -> str: def _load_bootstrap_files(self) -> str:
"""Load all bootstrap files from workspace.""" """Load all bootstrap files from workspace."""

View File

@ -6,17 +6,27 @@ from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from google.auth.exceptions import RefreshError
from google.auth.transport.requests import Request from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build from googleapiclient.discovery import build
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
from loguru import logger
from nanobot.agent.tools.base import Tool from nanobot.agent.tools.base import Tool
# Scopes required for Google Calendar API # Scopes required for Google Calendar API
SCOPES = ["https://www.googleapis.com/auth/calendar"] SCOPES = ["https://www.googleapis.com/auth/calendar"]
# Appended to failures so the model does not invent meetings when the API/auth did not succeed.
_NO_CALENDAR_DATA_FOR_MODEL = (
"\n\n[NO_CALENDAR_DATA] Google Calendar was not read successfully. "
"You MUST NOT infer, guess, or invent meetings, times, locations, or titles. "
"Tell the user only that calendar could not be accessed and what they should fix "
"(e.g. credentials_file, OAuth / calendar_token.json)."
)
class CalendarTool(Tool): class CalendarTool(Tool):
"""Tool to interact with Google Calendar.""" """Tool to interact with Google Calendar."""
@ -38,7 +48,9 @@ class CalendarTool(Tool):
"3. IMMEDIATELY call calendar(action='delete_events', event_ids=[...]) with the extracted IDs\n" "3. IMMEDIATELY call calendar(action='delete_events', event_ids=[...]) with the extracted IDs\n"
"NEVER use placeholder values like 'ID1', '[get event ID...]', or 'list_events'.\n" "NEVER use placeholder values like 'ID1', '[get event ID...]', or 'list_events'.\n"
"NEVER explain what you will do - just execute the tools immediately.\n" "NEVER explain what you will do - just execute the tools immediately.\n"
"When you call this tool, the system will execute it automatically. Do not show JSON in your response - just call the tool." "When you call this tool, the system will execute it automatically. Do not show JSON in your response - just call the tool.\n\n"
"If the tool result contains [NO_CALENDAR_DATA] or starts with 'Error:' and describes auth/API failure, "
"you have zero reliable calendar information—report the problem only; never make up events."
) )
def __init__(self, calendar_config: Any = None): def __init__(self, calendar_config: Any = None):
@ -50,6 +62,7 @@ class CalendarTool(Tool):
""" """
self._calendar_config = calendar_config self._calendar_config = calendar_config
self._service = None self._service = None
self._calendar_refresh_rejected = False
@property @property
def config(self) -> Any: def config(self) -> Any:
@ -73,6 +86,7 @@ class CalendarTool(Tool):
def _get_credentials(self) -> Credentials | None: def _get_credentials(self) -> Credentials | None:
"""Get valid user credentials from storage or OAuth flow.""" """Get valid user credentials from storage or OAuth flow."""
self._calendar_refresh_rejected = False
config = self.config config = self.config
if not config.enabled: if not config.enabled:
@ -106,6 +120,10 @@ class CalendarTool(Tool):
if creds and creds.expired and creds.refresh_token: if creds and creds.expired and creds.refresh_token:
try: try:
creds.refresh(Request()) creds.refresh(Request())
except RefreshError as e:
self._calendar_refresh_rejected = True
logger.warning("Google Calendar OAuth refresh failed: {}", e)
creds = None
except Exception: except Exception:
creds = None creds = None
@ -579,15 +597,26 @@ class CalendarTool(Tool):
config = self.config config = self.config
if not config.enabled: if not config.enabled:
return "Error: Calendar is not enabled. Set NANOBOT_TOOLS__CALENDAR__ENABLED=true" return (
"Error: Calendar is not enabled. Set NANOBOT_TOOLS__CALENDAR__ENABLED=true"
+ _NO_CALENDAR_DATA_FOR_MODEL
)
service = self._get_service() service = self._get_service()
if not service: if not service:
return ( msg = (
"Error: Could not authenticate with Google Calendar. " "Error: Could not authenticate with Google Calendar. "
"Please ensure credentials_file is configured and valid. " "Please ensure credentials_file is configured and valid. "
"You may need to run OAuth flow once to authorize access." "You may need to run OAuth flow once to authorize access."
) )
if self._calendar_refresh_rejected:
msg += (
" Google rejected the saved refresh token (expired or revoked). "
"Re-authorize on a machine with a browser and replace ~/.nanobot/calendar_token.json on the host, "
"then restart the container. If the token file is mounted :ro, refreshes cannot be persisted—"
"use rw or re-auth on the host when that happens."
)
return msg + _NO_CALENDAR_DATA_FOR_MODEL
calendar_id = config.calendar_id or "primary" calendar_id = config.calendar_id or "primary"
@ -679,9 +708,9 @@ class CalendarTool(Tool):
else: else:
return f"Error: Unknown action '{action}'. Use 'list_events', 'create_event', 'delete_event', 'delete_events', 'update_event', or 'check_availability'" return f"Error: Unknown action '{action}'. Use 'list_events', 'create_event', 'delete_event', 'delete_events', 'update_event', or 'check_availability'"
except HttpError as e: except HttpError as e:
return f"Error accessing Google Calendar API: {e}" return f"Error accessing Google Calendar API: {e}" + _NO_CALENDAR_DATA_FOR_MODEL
except Exception as e: except Exception as e:
return f"Error: {str(e)}" return f"Error: {str(e)}" + _NO_CALENDAR_DATA_FOR_MODEL
async def _list_events( async def _list_events(
self, service: Any, calendar_id: str, max_results: int, time_min: str | None self, service: Any, calendar_id: str, max_results: int, time_min: str | None

View File

@ -17,7 +17,27 @@ class EmailTool(Tool):
"""Read emails from configured IMAP mailbox.""" """Read emails from configured IMAP mailbox."""
name = "read_emails" name = "read_emails"
description = "USE THIS TOOL FOR ALL EMAIL QUERIES. When user asks about emails, latest email, email sender, inbox, attachments, etc., you MUST call read_emails(). DO NOT use mcp_gmail_mcp_read_email for emails received via email channel - use read_emails instead. DO NOT use exec() with mail/tail/awk commands. DO NOT use read_file() on /var/mail or memory files. DO NOT try alternative methods. This is the ONLY way to read emails from IMAP - it connects to IMAP and fetches real-time data. For 'latest email' or 'last email received' queries, use limit=1. When user asks to download attachments, use download_attachments=true. When user asks to find emails with a specific attachment (e.g., 'find email with attachment Rubiks'), use attachment_name='Rubiks'. CRITICAL: When user asks for specific fields like 'From and Subject' or 'sender and subject', extract and return ONLY those fields from the tool output. Do NOT summarize or analyze the email body content unless the user specifically asks for it. If user asks 'give me the from and subject', respond with just: 'From: [email] Subject: [subject]'. Parameters: limit (1-50, default 10, use 1 for latest), unread_only (bool, default false), mark_seen (bool, default false), download_attachments (bool, default false - set to true to download all attachments to workspace), attachment_name (string, optional - filter emails by attachment filename, case-insensitive partial match). Returns formatted email list with sender, subject, date, attachments (if any), downloaded file paths (if downloaded), and body." description = (
"USE THIS TOOL FOR OWNER-INITIATED MAILBOX QUERIES. When the user explicitly asks about their inbox "
"(e.g. latest email, email sender, unread emails, search inbox, attachments, etc.), you SHOULD call "
"read_emails(). DO NOT use mcp_gmail_mcp_read_email for emails received via the email channel - use "
"read_emails instead. DO NOT use exec() with mail/tail/awk commands. DO NOT use read_file() on /var/mail "
"or memory files. DO NOT try alternative methods. This is the ONLY way to read emails from IMAP - it connects "
"to IMAP and fetches real-time data. For 'latest email' or 'last email received' queries, use limit=1. "
"When the user asks to download attachments, use download_attachments=true. When the user asks to find "
"emails with a specific attachment (e.g., 'find email with attachment Rubiks'), use attachment_name='Rubiks'. "
"CRITICAL: When the user asks for specific fields like 'From and Subject' or 'sender and subject', extract "
"and return ONLY those fields from the tool output. Do NOT summarize or analyze the email body content unless "
"the user specifically asks for it. If the user asks 'give me the from and subject', respond with just: "
"'From: [email] Subject: [subject]'. VERY IMPORTANT: When replying on the email channel to a single incoming "
"email (content typically starts with 'Email received. From: ... Subject: ...'), treat that content as the "
"user's message and compose a direct reply. Do NOT call read_emails in that case unless the human explicitly "
"asks you to inspect or search the mailbox. Parameters: limit (1-50, default 10, use 1 for latest), "
"unread_only (bool, default false), mark_seen (bool, default false), download_attachments (bool, default false "
"- set to true to download all attachments to workspace), attachment_name (string, optional - filter emails by "
"attachment filename, case-insensitive partial match). Returns formatted email list with sender, subject, date, "
"attachments (if any), downloaded file paths (if downloaded), and body."
)
def __init__(self, email_config: Any = None): def __init__(self, email_config: Any = None):
""" """

View File

@ -115,7 +115,7 @@ class WriteFileTool(Tool):
@property @property
def description(self) -> str: def description(self) -> str:
return "Write content to a file at the given path. Creates parent directories if needed." return "Write content to a file at the given path. Creates parent directories if needed. IMPORTANT: Always provide both 'path' and 'content' parameters. If no full path is specified, use the workspace directory (/mnt/data/nanobot/workspace/)."
@property @property
def parameters(self) -> dict[str, Any]: def parameters(self) -> dict[str, Any]:

View File

@ -33,6 +33,7 @@ class MCPToolWrapper(Tool):
async def execute(self, **kwargs: Any) -> str: async def execute(self, **kwargs: Any) -> str:
from mcp import types from mcp import types
import json
result = await self._session.call_tool(self._original_name, arguments=kwargs) result = await self._session.call_tool(self._original_name, arguments=kwargs)
parts = [] parts = []
for block in result.content: for block in result.content:
@ -40,7 +41,28 @@ class MCPToolWrapper(Tool):
parts.append(block.text) parts.append(block.text)
else: else:
parts.append(str(block)) parts.append(str(block))
return "\n".join(parts) or "(no output)" output = "\n".join(parts)
# For empty results from search/list operations, provide clearer feedback
if not output or output.strip() == "":
# Check if this is a search/list operation (common patterns)
if "search" in self._original_name.lower() or "list" in self._original_name.lower():
if "unread" in str(kwargs).lower() or "is:unread" in str(kwargs).lower():
return "No unread emails found."
return "No results found."
# Try to parse JSON to check for empty arrays/lists
try:
parsed = json.loads(output)
if isinstance(parsed, list) and len(parsed) == 0:
if "search" in self._original_name.lower() or "list" in self._original_name.lower():
if "unread" in str(kwargs).lower() or "is:unread" in str(kwargs).lower():
return "No unread emails found."
return "No results found."
except (json.JSONDecodeError, ValueError):
pass # Not JSON, continue with original output
return output or "(no output)"
async def connect_mcp_servers( async def connect_mcp_servers(

View File

@ -276,7 +276,7 @@ class ToolsConfig(Base):
web: WebToolsConfig = Field(default_factory=WebToolsConfig) web: WebToolsConfig = Field(default_factory=WebToolsConfig)
exec: ExecToolConfig = Field(default_factory=ExecToolConfig) exec: ExecToolConfig = Field(default_factory=ExecToolConfig)
calendar: CalendarConfig = Field(default_factory=CalendarConfig) calendar: CalendarConfig = Field(default_factory=CalendarConfig)
restrict_to_workspace: bool = False # If true, restrict all tool access to workspace directory restrict_to_workspace: bool = True # If true, restrict all tool access to workspace directory
mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict) mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict)

104
update-multi-configs.sh Executable file
View File

@ -0,0 +1,104 @@
#!/bin/bash
# Script to update shared settings across all bot configs
set -e
CONFIG_DIRS=(
~/.nanobot-user1
~/.nanobot-user2
~/.nanobot-user3
)
# Function to update a specific key in all configs
update_config_key() {
local key_path="$1"
local new_value="$2"
echo "Updating $key_path to $new_value in all configs..."
for dir in "${CONFIG_DIRS[@]}"; do
config_file="$dir/config.json"
if [ -f "$config_file" ]; then
# Use jq to update the config
if command -v jq &> /dev/null; then
# Convert key_path like "providers.openrouter.apiKey" to jq path
jq_path=$(echo "$key_path" | sed 's/\./"."/g' | sed 's/^/./' | sed 's/\.$//')
jq "$jq_path = \"$new_value\"" "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
echo " ✓ Updated $config_file"
else
echo " ⚠ jq not found, skipping $config_file (install jq for automatic updates)"
fi
else
echo " ⚠ Config not found: $config_file"
fi
done
}
# Function to show usage
usage() {
cat << EOF
Usage: $0 <command> [args]
Commands:
update-api-key <provider> <key> Update API key for a provider (e.g., openrouter)
update-model <model> Update default model
update-setting <key.path> <value> Update any setting using dot notation
Examples:
$0 update-api-key openrouter "sk-or-v1-xxx"
$0 update-model "anthropic/claude-opus-4-5"
$0 update-setting "agents.defaults.temperature" "0.8"
Note: Requires 'jq' to be installed for automatic updates.
Install with: sudo apt install jq (or brew install jq on macOS)
EOF
}
# Main script
if [ $# -eq 0 ]; then
usage
exit 1
fi
case "$1" in
update-api-key)
if [ $# -ne 3 ]; then
echo "Error: update-api-key requires provider name and API key"
usage
exit 1
fi
provider="$2"
api_key="$3"
update_config_key "providers.$provider.apiKey" "$api_key"
;;
update-model)
if [ $# -ne 2 ]; then
echo "Error: update-model requires model name"
usage
exit 1
fi
model="$2"
update_config_key "agents.defaults.model" "$model"
;;
update-setting)
if [ $# -ne 3 ]; then
echo "Error: update-setting requires key path and value"
usage
exit 1
fi
key_path="$2"
value="$3"
update_config_key "$key_path" "$value"
;;
*)
echo "Unknown command: $1"
usage
exit 1
;;
esac
echo ""
echo "Done! Restart containers to apply changes:"
echo " docker compose -f docker-compose.multi.yml restart"