feat: multi-instance support with --config parameter

Add support for running multiple nanobot instances with complete isolation:

- Add --config parameter to gateway command for custom config file path
- Implement set_config_path() in config/loader.py for dynamic config path
- Derive data directory from config file location (e.g., ~/.nanobot-xxx/)
- Update get_data_path() to use unified data directory from config loader
- Ensure cron jobs use instance-specific data directory

This enables running multiple isolated nanobot instances by specifying
different config files, with each instance maintaining separate:
- Configuration files
- Workspace (memory, sessions, skills)
- Cron jobs
- Logs and media

Example usage:
  nanobot gateway --config ~/.nanobot-instance2/config.json --port 18791
This commit is contained in:
samsonchoi
2026-03-05 23:48:45 +08:00
parent c8f86fd052
commit 4e4d40ef33
3 changed files with 36 additions and 16 deletions

View File

@@ -244,15 +244,24 @@ def _make_provider(config: Config):
@app.command() @app.command()
def gateway( def gateway(
port: int = typer.Option(18790, "--port", "-p", help="Gateway port"), port: int = typer.Option(18790, "--port", "-p", help="Gateway port"),
workspace: str | None = typer.Option(None, "--workspace", "-w", help="Workspace directory"),
config: str | None = typer.Option(None, "--config", "-c", help="Config file path"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
config: str = typer.Option(None, "--config", "-c", help="Path to config file"),
): ):
"""Start the nanobot gateway.""" """Start the nanobot gateway."""
# Set config path if provided (must be done before any imports that use get_data_dir)
if config:
from nanobot.config.loader import set_config_path
config_path = Path(config).expanduser().resolve()
if not config_path.exists():
console.print(f"[red]Error: Config file not found: {config_path}[/red]")
raise typer.Exit(1)
set_config_path(config_path)
console.print(f"[dim]Using config: {config_path}[/dim]")
from nanobot.agent.loop import AgentLoop from nanobot.agent.loop import AgentLoop
from nanobot.bus.queue import MessageBus from nanobot.bus.queue import MessageBus
from nanobot.channels.manager import ChannelManager from nanobot.channels.manager import ChannelManager
from nanobot.config.loader import load_config from nanobot.config.loader import get_data_dir, load_config
from nanobot.cron.service import CronService from nanobot.cron.service import CronService
from nanobot.cron.types import CronJob from nanobot.cron.types import CronJob
from nanobot.heartbeat.service import HeartbeatService from nanobot.heartbeat.service import HeartbeatService
@@ -262,20 +271,16 @@ def gateway(
import logging import logging
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
config_path = Path(config) if config else None
config = load_config(config_path)
if workspace:
config.agents.defaults.workspace = workspace
console.print(f"{__logo__} Starting nanobot gateway on port {port}...") console.print(f"{__logo__} Starting nanobot gateway on port {port}...")
config = load_config()
sync_workspace_templates(config.workspace_path) sync_workspace_templates(config.workspace_path)
bus = MessageBus() bus = MessageBus()
provider = _make_provider(config) provider = _make_provider(config)
session_manager = SessionManager(config.workspace_path) session_manager = SessionManager(config.workspace_path)
# Create cron service first (callback set after agent creation) # Create cron service first (callback set after agent creation)
# Use workspace path for per-instance cron store cron_store_path = get_data_dir() / "cron" / "jobs.json"
cron_store_path = config.workspace_path / "cron" / "jobs.json"
cron = CronService(cron_store_path) cron = CronService(cron_store_path)
# Create agent with cron service # Create agent with cron service

View File

@@ -6,15 +6,29 @@ from pathlib import Path
from nanobot.config.schema import Config from nanobot.config.schema import Config
# Global variable to store current config path (for multi-instance support)
_current_config_path: Path | None = None
def set_config_path(path: Path) -> None:
"""Set the current config path (used to derive data directory)."""
global _current_config_path
_current_config_path = path
def get_config_path() -> Path: def get_config_path() -> Path:
"""Get the default configuration file path.""" """Get the configuration file path."""
if _current_config_path:
return _current_config_path
return Path.home() / ".nanobot" / "config.json" return Path.home() / ".nanobot" / "config.json"
def get_data_dir() -> Path: def get_data_dir() -> Path:
"""Get the nanobot data directory.""" """Get the nanobot data directory (derived from config path)."""
from nanobot.utils.helpers import get_data_path config_path = get_config_path()
return get_data_path() # If config is ~/.nanobot-xxx/config.json, data dir is ~/.nanobot-xxx/
# If config is ~/.nanobot/config.json, data dir is ~/.nanobot/
return config_path.parent
def load_config(config_path: Path | None = None) -> Config: def load_config(config_path: Path | None = None) -> Config:

View File

@@ -12,8 +12,9 @@ def ensure_dir(path: Path) -> Path:
def get_data_path() -> Path: def get_data_path() -> Path:
"""~/.nanobot data directory.""" """Get nanobot data directory (derived from config path)."""
return ensure_dir(Path.home() / ".nanobot") from nanobot.config.loader import get_data_dir
return ensure_dir(get_data_dir())
def get_workspace_path(workspace: str | None = None) -> Path: def get_workspace_path(workspace: str | None = None) -> Path: