Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
21 changes: 16 additions & 5 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
6 changes: 5 additions & 1 deletion config.local.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 17 additions & 3 deletions config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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", {})
Expand Down