diff --git a/README.md b/README.md index 68cdfb5e..d20e2052 100644 --- a/README.md +++ b/README.md @@ -388,6 +388,7 @@ Edit `config.toml` to customize agents, ports, and routing: [server] port = 8300 # web UI port host = "127.0.0.1" +trusted_origins = [] # optional HTTPS origins for reverse proxy / Tailscale access [agents.claude] command = "claude" # CLI command (must be on PATH) @@ -615,6 +616,143 @@ The session token is displayed in the terminal on startup and is only accessible > **`--allow-network` warning:** Network mode binds to a LAN IP, which exposes the server to your local network over unencrypted HTTP. Anyone on the same network can sniff the session token and gain full access — including the ability to @mention agents and trigger tool execution. If agents are running with auto-approve flags, this effectively grants remote code execution on your machine. **Only use `--allow-network` on a trusted home network. Never on public or shared WiFi.** +### Remote access via Tailscale + +If you want to open agentchattr from your phone or another personal device while it keeps running only on your local machine, use Tailscale instead of binding the app to a network interface. + +This is the recommended remote-access setup because: + +- agentchattr stays bound to `127.0.0.1` +- Tailscale provides the private HTTPS entrypoint +- only explicitly trusted HTTPS origins are allowed by the app + +#### What this requires + +- Tailscale installed on the host machine running agentchattr +- Tailscale installed on the remote device (Android, iPhone, laptop, etc.) +- both devices signed into the same tailnet + +#### 1. Install Tailscale + +**macOS** + +- Install Tailscale from the App Store, the official `.pkg`, or Homebrew: + ```bash + brew install --cask tailscale + ``` +- Open Tailscale and sign in. + +**Linux** + +- Install Tailscale using the official package instructions for your distro. +- Then bring it up: + ```bash + sudo tailscale up + ``` + +**Android** + +- Install the Tailscale app from Google Play. +- Sign in with the same tailnet account. + +#### 2. Keep agentchattr local-only + +Do **not** change the server bind to `0.0.0.0`. + +Keep `config.toml` like this: + +```toml +[server] +host = "127.0.0.1" +port = 8300 +``` + +This preserves the project's localhost-only security model. + +#### 3. Add the Tailscale HTTPS origin + +Create a local config file if you do not already have one: + +```bash +cp config.local.toml.example config.local.toml +``` + +Then add your Tailscale HTTPS hostname under `[server]` in `config.local.toml`: + +```toml +[server] +trusted_origins = ["https://your-machine.your-tailnet.ts.net"] +``` + +`config.local.toml` is gitignored, so your machine-specific tailnet hostname stays local. + +#### 4. Start agentchattr normally + +Start the app the same way you normally do: + +```bash +# Mac / Linux +./macos-linux/start.sh + +# or run an agent launcher, which starts the server if needed +./macos-linux/start_claude.sh +``` + +Or on Windows: + +```bat +windows\start.bat +``` + +#### 5. Enable Tailscale Serve + +Publish the local web UI through your tailnet: + +```bash +tailscale serve --bg 8300 +``` + +If Tailscale tells you `Serve is not enabled on your tailnet`, enable it from the URL it prints, then run the command again. + +Find the HTTPS URL: + +```bash +tailscale serve status +``` + +You should see something like: + +```text +https://your-machine.your-tailnet.ts.net (tailnet only) +|-- / proxy http://127.0.0.1:8300 +``` + +#### 6. Open from your phone + +- Make sure Tailscale is enabled on your phone. +- Open the Tailscale HTTPS URL in your mobile browser. +- If the page looks stale after CSS/UI changes, hard refresh or reopen the tab. + +#### Day-to-day usage + +- Start agentchattr normally via the launcher scripts. +- Keep Tailscale running on the host machine. +- `tailscale serve` stays active until you turn it off. +- Your remote URL remains the same unless your tailnet hostname changes. + +To disable the remote endpoint: + +```bash +tailscale serve --https=443 off +``` + +#### Security notes + +- Prefer Tailscale over `--allow-network`. +- Keep `host = "127.0.0.1"`. +- Only add exact HTTPS origins you control to `trusted_origins`. +- If you run agents with bypass / yolo / skip-permission modes, remember that remote access still gives real control over those agents. + ## Community Join the [Discord](https://discord.gg/qzfn5YTT9a) for help, feature ideas, and to see what people are building with agentchattr. diff --git a/app.py b/app.py index e8d8e407..1455b852 100644 --- a/app.py +++ b/app.py @@ -167,15 +167,26 @@ def _resolve_authenticated_agent(request: Request) -> dict | None: _PUBLIC_PREFIXES = ("/", "/static/") +def _build_allowed_origins(cfg: dict) -> set[str]: + server_cfg = cfg.get("server", {}) + port = server_cfg.get("port", 8300) + allowed = { + f"http://127.0.0.1:{port}", + f"http://localhost:{port}", + } + for origin in server_cfg.get("trusted_origins", []): + if isinstance(origin, str): + cleaned = origin.strip().rstrip("/") + if cleaned: + allowed.add(cleaned) + return allowed + + def _install_security_middleware(token: str, cfg: dict): """Add token validation and origin checking middleware to the app.""" import app as _self _self.session_token = token - port = cfg.get("server", {}).get("port", 8300) - allowed_origins = { - f"http://127.0.0.1:{port}", - f"http://localhost:{port}", - } + allowed_origins = _build_allowed_origins(cfg) class SecurityMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): diff --git a/config.local.toml.example b/config.local.toml.example index 763face4..ef688761 100644 --- a/config.local.toml.example +++ b/config.local.toml.example @@ -6,11 +6,15 @@ # # Usage: # 1. cp config.local.toml.example config.local.toml -# 2. Edit the [agents.NAME] section below +# 2. Edit [server] and/or the [agents.NAME] section below # 3. Start the server: python run.py # 4. Start the wrapper: python wrapper_api.py NAME # (or use: windows/start_api_agent.bat NAME) +# --- Optional: trusted browser origins for Tailscale / reverse proxy access --- +# [server] +# trusted_origins = ["https://your-machine.your-tailnet.ts.net"] + # --- Example: Qwen via llama-server --- # [agents.qwen] # type = "api" # required — marks this as an API agent diff --git a/config.toml b/config.toml index ea58875e..6c560265 100644 --- a/config.toml +++ b/config.toml @@ -2,6 +2,7 @@ port = 8300 host = "127.0.0.1" data_dir = "./data" +trusted_origins = [] # Optional HTTPS origins for reverse proxy / Tailscale access # Add agents here. Each gets a status pill, @mention routing, and color. # "cwd" is the working directory for the agent's terminal session. diff --git a/config_loader.py b/config_loader.py index 4c2c0636..8fd57a48 100644 --- a/config_loader.py +++ b/config_loader.py @@ -104,9 +104,10 @@ def load_config(root: Path | None = None) -> dict: """Load config.toml and merge config.local.toml if it exists. config.local.toml is gitignored and intended for user-specific agents - (e.g. local LLM endpoints) that shouldn't be committed. - Only the [agents] section is merged — local entries are added alongside - (not replacing) the agents defined in config.toml. + (e.g. local LLM endpoints or trusted reverse-proxy origins) that + shouldn't be committed. Only safe local-only settings are merged: + [agents] entries and [server].trusted_origins. Local agent entries are + added alongside (not replacing) the agents defined in config.toml. AGENTCHATTR_* environment variables override values from config.toml (see module docstring for the list). @@ -122,6 +123,19 @@ def load_config(root: Path | None = None) -> dict: with open(local_path, "rb") as f: local = tomllib.load(f) + local_server = local.get("server", {}) + local_trusted_origins = local_server.get("trusted_origins") + if isinstance(local_trusted_origins, list): + server_cfg = config.setdefault("server", {}) + merged = [] + for origin in [ + *server_cfg.get("trusted_origins", []), + *local_trusted_origins, + ]: + if isinstance(origin, str) and origin not in merged: + merged.append(origin) + server_cfg["trusted_origins"] = merged + # Merge [agents] section — local agents are added ONLY if they don't already exist. # This protects the "holy trinity" (claude, codex, gemini) from being overridden. local_agents = local.get("agents", {})