# GoClaw Ingress Configuration Guide

Ingress is the public entry point for webhooks and channel callbacks. It lets platforms such as Microsoft Teams, Facebook, GitLab, Graylog, and custom webhook senders reach a GoClaw process that is running on a local or private port.

## Request Flow

```mermaid
flowchart TD
    EXT["<b>External Platform</b><br/>Teams, Facebook, GitLab, Graylog, custom webhook"] --> ING["<b>Ingress Layer</b><br/>ngrok, nginx, Caddy, HAProxy, or another reverse proxy"]
    ING --> GW["<b>GoClaw Gateway or Dedicated Webhook Listener</b><br/>• /hook/msteams, /hook/facebook, /hook/telegram<br/>• /hook/&lt;slug&gt;/msteams, /hook/&lt;slug&gt;/facebook, /hook/&lt;slug&gt;/telegram<br/>• /goclaw/hook/&lt;type&gt;[/&lt;key&gt;]<br/>• /api/webhook"]
```

## Config Fields

```yaml
gateway:
  ingress:
    type: ""               # "ngrok", "proxy", "nginx", or "" (no tunnel; see Ingress Types)
    url: ""                # The address outside systems reach this gateway at
    authtoken: ""          # Ngrok auth token, only used with type: "ngrok"
    allow_preset: ""       # "microsoft-teams", "none", or empty
    allow_ips: []          # CIDR/IP allowlist; when set, it replaces the preset
    port: ""               # Optional dedicated webhook port
    bind: ""               # Webhook port's interface: "loopback" (default), "lan", or an explicit IP
    trust_proxy: false     # Trust X-Forwarded-For / X-Real-IP from a known proxy
```

If `gateway.ingress.port` is empty, webhook routes share the main gateway port. If it is set, GoClaw starts a separate webhook server for ingress traffic. This is useful when the WebUI and admin API should stay private while public webhooks are exposed. That server listens on `gateway.ingress.bind`, which defaults to loopback: set `bind: "lan"` (or an IP) when the sender reaches it directly rather than through a proxy on the same host.

With a dedicated port, the webhook routes (`/hook/*` and `/goclaw/hook/*`) leave the Web UI port: they are served only on `gateway.ingress.port`, and a hook URL that points at `server.port` gets `404`. Health probes and `/api/webhook` are served on both ports; `/api/webhook` needs the gateway token on either. The Web UI, `/gateway` and the rest of `/api/*` stay on `server.port` only.

`url` is the gateway's address as the sender sees it, including a proxy's path prefix if it has one. Features that give this address to another system read it whatever `type` is: the Webhooks page, the agent's `graylog_alert` tool (Graylog calls `<url>/goclaw/hook/graylog/<key>`) and the `goclaw gitlab hook` wizard. On a LAN with no proxy it can be the gateway's own address, for example `http://192.0.2.10:8080`; the GitLab wizard, though, accepts only an `https://` address.

## Ingress Types

| Type | Behavior |
|---|---|
| empty | No tunnel is started. `url`, when set, is still the address the Webhooks page, `graylog_alert` and the GitLab wizard give senders; without it the Webhooks page builds hook URLs from the address you opened it at. This does not limit who can reach the gateway: that is wherever `gateway.bind` (or `gateway.ingress.bind`, with a dedicated `port`) listens, subject to `allow_ips` / `allow_preset` |
| `ngrok` | GoClaw starts or uses an ngrok tunnel for webhook traffic |
| `proxy` | An external reverse proxy forwards traffic to GoClaw |
| `nginx` | Alias-style value for reverse proxy deployments |

## Common Routes

| Route | Purpose |
|---|---|
| `/hook/msteams` | Microsoft Teams channel webhook |
| `/hook/facebook` | Facebook channel webhook |
| `/hook/telegram` | Telegram channel webhook (only with `channels.telegram.mode: webhook`) |
| `/hook/<slug>/msteams`, `/hook/<slug>/facebook`, `/hook/<slug>/telegram` | The same webhooks when `gateway.webhook_secret_slug` is set: the plain `/hook/<channel>` path is then not served |
| `/goclaw/hook/<type>[/<key>]` | Generic webhook router |
| `/api/webhook` | Inbound message API: the agent answers a text as if it came from the named channel (`goclaw message` uses it) |
| `/api/send` | Outbound message API for local scripts: main gateway port only, loopback callers only, with the gateway token. Not a route to expose |
| `/gateway` | Web chat WebSocket when the main gateway is exposed |

## Ngrok For Testing

```yaml
gateway:
  ingress:
    type: "ngrok"
    authtoken: "${NGROK_AUTHTOKEN}"
    url: "https://my-agent.ngrok.app"
    port: "8001"
    allow_preset: "microsoft-teams"
```

The built-in tunnel delivers requests to GoClaw from the local machine (loopback), which the allowlist always lets through, so with `trust_proxy: false` (the default) `allow_preset` does not filter ngrok traffic. The Teams webhook is still protected by its Bot Framework token check (see [Microsoft Teams](channels/msteams.md)). The allowlist filters callers that connect directly or through a reverse proxy.

Use a stable ngrok domain when the external platform stores a fixed callback URL. For Microsoft Teams, set the Azure Bot messaging endpoint to:

```text
https://my-agent.ngrok.app/hook/msteams
```

or, with a secret slug:

```text
https://my-agent.ngrok.app/hook/<slug>/msteams
```

## Reverse Proxy With Path Prefixes

One domain can serve multiple agents by prefix. The proxy must strip the prefix before forwarding to GoClaw.

```text
https://agents.example.com/finance/    -> 127.0.0.1:8001
https://agents.example.com/analyst/   -> 127.0.0.1:8002
```

Agent config:

```yaml
gateway:
  bind: "loopback"
  ingress:
    type: "nginx"
    url: "https://agents.example.com/finance"
    port: "8001"
    bind: "loopback"
    allow_preset: "microsoft-teams"
    trust_proxy: true
```

Nginx example:

```nginx
location /finance/ {
    proxy_pass http://127.0.0.1:8001/;
    proxy_http_version 1.1;

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
    proxy_buffering off;
}

location = /finance {
    return 301 $scheme://$host/finance/;
}
```

The trailing slash in `proxy_pass http://127.0.0.1:8001/;` is important. It strips `/finance/` before forwarding, so `/finance/health` reaches GoClaw as `/health`.

## Reverse Proxy With Subdomains

Use a subdomain per agent when DNS and TLS automation are already in place.

```nginx
server {
    server_name finance.agents.example.com;

    location / {
        proxy_pass http://127.0.0.1:8001/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_read_timeout 300s;
        proxy_buffering off;
    }
}
```

```yaml
gateway:
  ingress:
    type: "nginx"
    url: "https://finance.agents.example.com"
    port: "8001"
    bind: "loopback"
    trust_proxy: true
```

## Allowlist And Proxy Trust

`allow_preset: "microsoft-teams"` admits the Microsoft 365 address ranges Teams calls from. GoClaw downloads them from `endpoints.office.com` at start-up; if the download fails, the gateway does not start (`Ingress failed: …`), so the host needs outbound HTTPS to that address.

`allow_ips` is a list of IPs and CIDR ranges. When it has any entry, it **replaces** the preset: the Teams ranges are not loaded, and only the listed addresses get in. To admit Teams and your own addresses, use one of these:

- Leave `allow_ips` empty and keep the preset. Callers on loopback are always allowed, so health checks and local tools still get in. Behind a reverse proxy on the same host, set `trust_proxy: true`: the allowlist then judges the client address the proxy forwards, not the proxy's own loopback address.
- List everything yourself in `allow_ips`: the Teams ranges (Microsoft publishes them at `endpoints.office.com`, service area "Skype") and your own proxy or test addresses.

```yaml
gateway:
  ingress:
    allow_ips:                # no allow_preset: these are the only addresses let in
      - "10.0.1.5"            # the reverse proxy
      - "203.0.113.10/32"     # a test machine
```

The allowlist covers every request on the listener it guards. With `gateway.ingress.port` set, that is the dedicated webhook listener. Without it, webhooks share the main port, and the allowlist covers the Web UI and the API there too: a browser on an address that is not listed gets `403`.

Set `trust_proxy: true` only when the immediate upstream is a proxy you control, and have that proxy set or append `X-Forwarded-For` with the client address it saw (`proxy_set_header X-Forwarded-For $remote_addr;` in nginx, as the examples above do; `$proxy_add_x_forwarded_for` works too). GoClaw takes the **last** entry, the one your proxy wrote: anything before it is what the caller sent. Otherwise, GoClaw uses the TCP remote address and ignores spoofable forwarding headers.

Loopback callers are always allowed for local health checks and reverse proxy deployments.

## Troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| Prefix URL returns 404 | The reverse proxy did not strip the prefix | Use a trailing slash in `proxy_pass` |
| WebSocket does not connect | `Upgrade` and `Connection` headers are missing | Forward WebSocket headers |
| Teams endpoint validation fails | Wrong path or method filtering in proxy | Forward all methods to GoClaw; let GoClaw validate |
| Requests are denied by allowlist | GoClaw sees the proxy IP, not the external IP | Enable `trust_proxy` for a trusted proxy, or add the proxy IP to `allow_ips` (which replaces `allow_preset`) |
| Gateway exits at start-up with `Ingress failed` | `allow_preset: microsoft-teams` could not download the Microsoft 365 ranges | Allow outbound HTTPS to `endpoints.office.com`, or list the ranges in `allow_ips` |
| Hook URL returns 404 | `gateway.ingress.port` is set and the URL points at `server.port` | Point the sender (or the proxy) at the webhook port |
| WebUI is public unintentionally | Webhook and WebUI share the same public listener | Use `gateway.ingress.port` and expose only that port |

## Implementation Files

- `internal/config/config.go`
- `internal/config/config_loader.go`
- `cmd/gateway/main.go`
- `cmd/gateway/startup.go`
- `internal/gateway/handler_api.go`
- `internal/runtimeinfo/runtimeinfo.go`
