mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-10 23:30:53 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
186
openclaw/docs/gateway/authentication.md
Normal file
186
openclaw/docs/gateway/authentication.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
---
|
||||
summary: "Model authentication: OAuth, API keys, Claude CLI reuse, and Anthropic setup-token"
|
||||
read_when:
|
||||
- Debugging model auth or OAuth expiry
|
||||
- Documenting authentication or credential storage
|
||||
title: "Authentication"
|
||||
---
|
||||
|
||||
# Authentication (Model Providers)
|
||||
|
||||
<Note>
|
||||
This page covers **model provider** authentication (API keys, OAuth, Claude CLI reuse, and Anthropic setup-token). For **gateway connection** authentication (token, password, trusted-proxy), see [Configuration](/gateway/configuration) and [Trusted Proxy Auth](/gateway/trusted-proxy-auth).
|
||||
</Note>
|
||||
|
||||
OpenClaw supports OAuth and API keys for model providers. For always-on gateway
|
||||
hosts, API keys are usually the most predictable option. Subscription/OAuth
|
||||
flows are also supported when they match your provider account model.
|
||||
|
||||
See [/concepts/oauth](/concepts/oauth) for the full OAuth flow and storage
|
||||
layout.
|
||||
For SecretRef-based auth (`env`/`file`/`exec` providers), see [Secrets Management](/gateway/secrets).
|
||||
For credential eligibility/reason-code rules used by `models status --probe`, see
|
||||
[Auth Credential Semantics](/auth-credential-semantics).
|
||||
|
||||
## Recommended setup (API key, any provider)
|
||||
|
||||
If you’re running a long-lived gateway, start with an API key for your chosen
|
||||
provider.
|
||||
For Anthropic specifically, API key auth is still the most predictable server
|
||||
setup, but OpenClaw also supports reusing a local Claude CLI login.
|
||||
|
||||
1. Create an API key in your provider console.
|
||||
2. Put it on the **gateway host** (the machine running `openclaw gateway`).
|
||||
|
||||
```bash
|
||||
export <PROVIDER>_API_KEY="..."
|
||||
openclaw models status
|
||||
```
|
||||
|
||||
3. If the Gateway runs under systemd/launchd, prefer putting the key in
|
||||
`~/.openclaw/.env` so the daemon can read it:
|
||||
|
||||
```bash
|
||||
cat >> ~/.openclaw/.env <<'EOF'
|
||||
<PROVIDER>_API_KEY=...
|
||||
EOF
|
||||
```
|
||||
|
||||
Then restart the daemon (or restart your Gateway process) and re-check:
|
||||
|
||||
```bash
|
||||
openclaw models status
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
If you’d rather not manage env vars yourself, onboarding can store
|
||||
API keys for daemon use: `openclaw onboard`.
|
||||
|
||||
See [Help](/help) for details on env inheritance (`env.shellEnv`,
|
||||
`~/.openclaw/.env`, systemd/launchd).
|
||||
|
||||
## Anthropic: Claude CLI and token compatibility
|
||||
|
||||
Anthropic setup-token auth is still available in OpenClaw as a supported token
|
||||
path. Anthropic staff has since told us that OpenClaw-style Claude CLI usage is
|
||||
allowed again, so OpenClaw treats Claude CLI reuse and `claude -p` usage as
|
||||
sanctioned for this integration unless Anthropic publishes a new policy. When
|
||||
Claude CLI reuse is available on the host, that is now the preferred path.
|
||||
|
||||
For long-lived gateway hosts, an Anthropic API key is still the most predictable
|
||||
setup. If you want to reuse an existing Claude login on the same host, use the
|
||||
Anthropic Claude CLI path in onboarding/configure.
|
||||
|
||||
Manual token entry (any provider; writes `auth-profiles.json` + updates config):
|
||||
|
||||
```bash
|
||||
openclaw models auth paste-token --provider openrouter
|
||||
```
|
||||
|
||||
Auth profile refs are also supported for static credentials:
|
||||
|
||||
- `api_key` credentials can use `keyRef: { source, provider, id }`
|
||||
- `token` credentials can use `tokenRef: { source, provider, id }`
|
||||
- OAuth-mode profiles do not support SecretRef credentials; if `auth.profiles.<id>.mode` is set to `"oauth"`, SecretRef-backed `keyRef`/`tokenRef` input for that profile is rejected.
|
||||
|
||||
Automation-friendly check (exit `1` when expired/missing, `2` when expiring):
|
||||
|
||||
```bash
|
||||
openclaw models status --check
|
||||
```
|
||||
|
||||
Live auth probes:
|
||||
|
||||
```bash
|
||||
openclaw models status --probe
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Probe rows can come from auth profiles, env credentials, or `models.json`.
|
||||
- If explicit `auth.order.<provider>` omits a stored profile, probe reports
|
||||
`excluded_by_auth_order` for that profile instead of trying it.
|
||||
- If auth exists but OpenClaw cannot resolve a probeable model candidate for
|
||||
that provider, probe reports `status: no_model`.
|
||||
- Rate-limit cooldowns can be model-scoped. A profile cooling down for one
|
||||
model can still be usable for a sibling model on the same provider.
|
||||
|
||||
Optional ops scripts (systemd/Termux) are documented here:
|
||||
[Auth monitoring scripts](/help/scripts#auth-monitoring-scripts)
|
||||
|
||||
## Anthropic note
|
||||
|
||||
The Anthropic `claude-cli` backend is supported again.
|
||||
|
||||
- Anthropic staff told us this OpenClaw integration path is allowed again.
|
||||
- OpenClaw therefore treats Claude CLI reuse and `claude -p` usage as sanctioned
|
||||
for Anthropic-backed runs unless Anthropic publishes a new policy.
|
||||
- Anthropic API keys remain the most predictable choice for long-lived gateway
|
||||
hosts and explicit server-side billing control.
|
||||
|
||||
## Checking model auth status
|
||||
|
||||
```bash
|
||||
openclaw models status
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
## API key rotation behavior (gateway)
|
||||
|
||||
Some providers support retrying a request with alternative keys when an API call
|
||||
hits a provider rate limit.
|
||||
|
||||
- Priority order:
|
||||
- `OPENCLAW_LIVE_<PROVIDER>_KEY` (single override)
|
||||
- `<PROVIDER>_API_KEYS`
|
||||
- `<PROVIDER>_API_KEY`
|
||||
- `<PROVIDER>_API_KEY_*`
|
||||
- Google providers also include `GOOGLE_API_KEY` as an additional fallback.
|
||||
- The same key list is deduplicated before use.
|
||||
- OpenClaw retries with the next key only for rate-limit errors (for example
|
||||
`429`, `rate_limit`, `quota`, `resource exhausted`, `Too many concurrent
|
||||
requests`, `ThrottlingException`, `concurrency limit reached`, or
|
||||
`workers_ai ... quota limit exceeded`).
|
||||
- Non-rate-limit errors are not retried with alternate keys.
|
||||
- If all keys fail, the final error from the last attempt is returned.
|
||||
|
||||
## Controlling which credential is used
|
||||
|
||||
### Per-session (chat command)
|
||||
|
||||
Use `/model <alias-or-id>@<profileId>` to pin a specific provider credential for the current session (example profile ids: `anthropic:default`, `anthropic:work`).
|
||||
|
||||
Use `/model` (or `/model list`) for a compact picker; use `/model status` for the full view (candidates + next auth profile, plus provider endpoint details when configured).
|
||||
|
||||
### Per-agent (CLI override)
|
||||
|
||||
Set an explicit auth profile order override for an agent (stored in that agent’s `auth-state.json`):
|
||||
|
||||
```bash
|
||||
openclaw models auth order get --provider anthropic
|
||||
openclaw models auth order set --provider anthropic anthropic:default
|
||||
openclaw models auth order clear --provider anthropic
|
||||
```
|
||||
|
||||
Use `--agent <id>` to target a specific agent; omit it to use the configured default agent.
|
||||
When you debug order issues, `openclaw models status --probe` shows omitted
|
||||
stored profiles as `excluded_by_auth_order` instead of silently skipping them.
|
||||
When you debug cooldown issues, remember that rate-limit cooldowns can be tied
|
||||
to one model id rather than the whole provider profile.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No credentials found"
|
||||
|
||||
If the Anthropic profile is missing, configure an Anthropic API key on the
|
||||
**gateway host** or set up the Anthropic setup-token path, then re-check:
|
||||
|
||||
```bash
|
||||
openclaw models status
|
||||
```
|
||||
|
||||
### Token expiring/expired
|
||||
|
||||
Run `openclaw models status` to confirm which profile is expiring. If an
|
||||
Anthropic token profile is missing or expired, refresh that setup via
|
||||
setup-token or migrate to an Anthropic API key.
|
||||
131
openclaw/docs/gateway/background-process.md
Normal file
131
openclaw/docs/gateway/background-process.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
---
|
||||
summary: "Background exec execution and process management"
|
||||
read_when:
|
||||
- Adding or modifying background exec behavior
|
||||
- Debugging long-running exec tasks
|
||||
title: "Background Exec and Process Tool"
|
||||
---
|
||||
|
||||
# Background Exec + Process Tool
|
||||
|
||||
OpenClaw runs shell commands through the `exec` tool and keeps long‑running tasks in memory. The `process` tool manages those background sessions.
|
||||
|
||||
## exec tool
|
||||
|
||||
Key parameters:
|
||||
|
||||
- `command` (required)
|
||||
- `yieldMs` (default 10000): auto‑background after this delay
|
||||
- `background` (bool): background immediately
|
||||
- `timeout` (seconds, default 1800): kill the process after this timeout
|
||||
- `elevated` (bool): run outside the sandbox if elevated mode is enabled/allowed (`gateway` by default, or `node` when the exec target is `node`)
|
||||
- Need a real TTY? Set `pty: true`.
|
||||
- `workdir`, `env`
|
||||
|
||||
Behavior:
|
||||
|
||||
- Foreground runs return output directly.
|
||||
- When backgrounded (explicit or timeout), the tool returns `status: "running"` + `sessionId` and a short tail.
|
||||
- Output is kept in memory until the session is polled or cleared.
|
||||
- If the `process` tool is disallowed, `exec` runs synchronously and ignores `yieldMs`/`background`.
|
||||
- Spawned exec commands receive `OPENCLAW_SHELL=exec` for context-aware shell/profile rules.
|
||||
- For long-running work that starts now, start it once and rely on automatic
|
||||
completion wake when it is enabled and the command emits output or fails.
|
||||
- If automatic completion wake is unavailable, or you need quiet-success
|
||||
confirmation for a command that exited cleanly without output, use `process`
|
||||
to confirm completion.
|
||||
- Do not emulate reminders or delayed follow-ups with `sleep` loops or repeated
|
||||
polling; use cron for future work.
|
||||
|
||||
## Child process bridging
|
||||
|
||||
When spawning long-running child processes outside the exec/process tools (for example, CLI respawns or gateway helpers), attach the child-process bridge helper so termination signals are forwarded and listeners are detached on exit/error. This avoids orphaned processes on systemd and keeps shutdown behavior consistent across platforms.
|
||||
|
||||
Environment overrides:
|
||||
|
||||
- `PI_BASH_YIELD_MS`: default yield (ms)
|
||||
- `PI_BASH_MAX_OUTPUT_CHARS`: in‑memory output cap (chars)
|
||||
- `OPENCLAW_BASH_PENDING_MAX_OUTPUT_CHARS`: pending stdout/stderr cap per stream (chars)
|
||||
- `PI_BASH_JOB_TTL_MS`: TTL for finished sessions (ms, bounded to 1m–3h)
|
||||
|
||||
Config (preferred):
|
||||
|
||||
- `tools.exec.backgroundMs` (default 10000)
|
||||
- `tools.exec.timeoutSec` (default 1800)
|
||||
- `tools.exec.cleanupMs` (default 1800000)
|
||||
- `tools.exec.notifyOnExit` (default true): enqueue a system event + request heartbeat when a backgrounded exec exits.
|
||||
- `tools.exec.notifyOnExitEmptySuccess` (default false): when true, also enqueue completion events for successful backgrounded runs that produced no output.
|
||||
|
||||
## process tool
|
||||
|
||||
Actions:
|
||||
|
||||
- `list`: running + finished sessions
|
||||
- `poll`: drain new output for a session (also reports exit status)
|
||||
- `log`: read the aggregated output (supports `offset` + `limit`)
|
||||
- `write`: send stdin (`data`, optional `eof`)
|
||||
- `send-keys`: send explicit key tokens or bytes to a PTY-backed session
|
||||
- `submit`: send Enter / carriage return to a PTY-backed session
|
||||
- `paste`: send literal text, optionally wrapped in bracketed paste mode
|
||||
- `kill`: terminate a background session
|
||||
- `clear`: remove a finished session from memory
|
||||
- `remove`: kill if running, otherwise clear if finished
|
||||
|
||||
Notes:
|
||||
|
||||
- Only backgrounded sessions are listed/persisted in memory.
|
||||
- Sessions are lost on process restart (no disk persistence).
|
||||
- Session logs are only saved to chat history if you run `process poll/log` and the tool result is recorded.
|
||||
- `process` is scoped per agent; it only sees sessions started by that agent.
|
||||
- Use `poll` / `log` for status, logs, quiet-success confirmation, or
|
||||
completion confirmation when automatic completion wake is unavailable.
|
||||
- Use `write` / `send-keys` / `submit` / `paste` / `kill` when you need input
|
||||
or intervention.
|
||||
- `process list` includes a derived `name` (command verb + target) for quick scans.
|
||||
- `process log` uses line-based `offset`/`limit`.
|
||||
- When both `offset` and `limit` are omitted, it returns the last 200 lines and includes a paging hint.
|
||||
- When `offset` is provided and `limit` is omitted, it returns from `offset` to the end (not capped to 200).
|
||||
- Polling is for on-demand status, not wait-loop scheduling. If the work should
|
||||
happen later, use cron instead.
|
||||
|
||||
## Examples
|
||||
|
||||
Run a long task and poll later:
|
||||
|
||||
```json
|
||||
{ "tool": "exec", "command": "sleep 5 && echo done", "yieldMs": 1000 }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "tool": "process", "action": "poll", "sessionId": "<id>" }
|
||||
```
|
||||
|
||||
Start immediately in background:
|
||||
|
||||
```json
|
||||
{ "tool": "exec", "command": "npm run build", "background": true }
|
||||
```
|
||||
|
||||
Send stdin:
|
||||
|
||||
```json
|
||||
{ "tool": "process", "action": "write", "sessionId": "<id>", "data": "y\n" }
|
||||
```
|
||||
|
||||
Send PTY keys:
|
||||
|
||||
```json
|
||||
{ "tool": "process", "action": "send-keys", "sessionId": "<id>", "keys": ["C-c"] }
|
||||
```
|
||||
|
||||
Submit current line:
|
||||
|
||||
```json
|
||||
{ "tool": "process", "action": "submit", "sessionId": "<id>" }
|
||||
```
|
||||
|
||||
Paste literal text:
|
||||
|
||||
```json
|
||||
{ "tool": "process", "action": "paste", "sessionId": "<id>", "text": "line1\nline2\n" }
|
||||
```
|
||||
179
openclaw/docs/gateway/bonjour.md
Normal file
179
openclaw/docs/gateway/bonjour.md
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
---
|
||||
summary: "Bonjour/mDNS discovery + debugging (Gateway beacons, clients, and common failure modes)"
|
||||
read_when:
|
||||
- Debugging Bonjour discovery issues on macOS/iOS
|
||||
- Changing mDNS service types, TXT records, or discovery UX
|
||||
title: "Bonjour Discovery"
|
||||
---
|
||||
|
||||
# Bonjour / mDNS discovery
|
||||
|
||||
OpenClaw uses Bonjour (mDNS / DNS‑SD) to discover an active Gateway (WebSocket endpoint).
|
||||
Multicast `local.` browsing is a **LAN-only convenience**. For cross-network discovery, the
|
||||
same beacon can also be published through a configured wide-area DNS-SD domain. Discovery is
|
||||
still best-effort and does **not** replace SSH or Tailnet-based connectivity.
|
||||
|
||||
## Wide-area Bonjour (Unicast DNS-SD) over Tailscale
|
||||
|
||||
If the node and gateway are on different networks, multicast mDNS won’t cross the
|
||||
boundary. You can keep the same discovery UX by switching to **unicast DNS‑SD**
|
||||
("Wide‑Area Bonjour") over Tailscale.
|
||||
|
||||
High‑level steps:
|
||||
|
||||
1. Run a DNS server on the gateway host (reachable over Tailnet).
|
||||
2. Publish DNS‑SD records for `_openclaw-gw._tcp` under a dedicated zone
|
||||
(example: `openclaw.internal.`).
|
||||
3. Configure Tailscale **split DNS** so your chosen domain resolves via that
|
||||
DNS server for clients (including iOS).
|
||||
|
||||
OpenClaw supports any discovery domain; `openclaw.internal.` is just an example.
|
||||
iOS/Android nodes browse both `local.` and your configured wide‑area domain.
|
||||
|
||||
### Gateway config (recommended)
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: { bind: "tailnet" }, // tailnet-only (recommended)
|
||||
discovery: { wideArea: { enabled: true } }, // enables wide-area DNS-SD publishing
|
||||
}
|
||||
```
|
||||
|
||||
### One-time DNS server setup (gateway host)
|
||||
|
||||
```bash
|
||||
openclaw dns setup --apply
|
||||
```
|
||||
|
||||
This installs CoreDNS and configures it to:
|
||||
|
||||
- listen on port 53 only on the gateway’s Tailscale interfaces
|
||||
- serve your chosen domain (example: `openclaw.internal.`) from `~/.openclaw/dns/<domain>.db`
|
||||
|
||||
Validate from a tailnet‑connected machine:
|
||||
|
||||
```bash
|
||||
dns-sd -B _openclaw-gw._tcp openclaw.internal.
|
||||
dig @<TAILNET_IPV4> -p 53 _openclaw-gw._tcp.openclaw.internal PTR +short
|
||||
```
|
||||
|
||||
### Tailscale DNS settings
|
||||
|
||||
In the Tailscale admin console:
|
||||
|
||||
- Add a nameserver pointing at the gateway’s tailnet IP (UDP/TCP 53).
|
||||
- Add split DNS so your discovery domain uses that nameserver.
|
||||
|
||||
Once clients accept tailnet DNS, iOS nodes and CLI discovery can browse
|
||||
`_openclaw-gw._tcp` in your discovery domain without multicast.
|
||||
|
||||
### Gateway listener security (recommended)
|
||||
|
||||
The Gateway WS port (default `18789`) binds to loopback by default. For LAN/tailnet
|
||||
access, bind explicitly and keep auth enabled.
|
||||
|
||||
For tailnet‑only setups:
|
||||
|
||||
- Set `gateway.bind: "tailnet"` in `~/.openclaw/openclaw.json`.
|
||||
- Restart the Gateway (or restart the macOS menubar app).
|
||||
|
||||
## What advertises
|
||||
|
||||
Only the Gateway advertises `_openclaw-gw._tcp`.
|
||||
|
||||
## Service types
|
||||
|
||||
- `_openclaw-gw._tcp` — gateway transport beacon (used by macOS/iOS/Android nodes).
|
||||
|
||||
## TXT keys (non-secret hints)
|
||||
|
||||
The Gateway advertises small non‑secret hints to make UI flows convenient:
|
||||
|
||||
- `role=gateway`
|
||||
- `displayName=<friendly name>`
|
||||
- `lanHost=<hostname>.local`
|
||||
- `gatewayPort=<port>` (Gateway WS + HTTP)
|
||||
- `gatewayTls=1` (only when TLS is enabled)
|
||||
- `gatewayTlsSha256=<sha256>` (only when TLS is enabled and fingerprint is available)
|
||||
- `canvasPort=<port>` (only when the canvas host is enabled; currently the same as `gatewayPort`)
|
||||
- `transport=gateway`
|
||||
- `tailnetDns=<magicdns>` (optional hint when Tailnet is available)
|
||||
- `sshPort=<port>` (mDNS full mode only; wide-area DNS-SD may omit it)
|
||||
- `cliPath=<path>` (mDNS full mode only; wide-area DNS-SD still writes it as a remote-install hint)
|
||||
|
||||
Security notes:
|
||||
|
||||
- Bonjour/mDNS TXT records are **unauthenticated**. Clients must not treat TXT as authoritative routing.
|
||||
- Clients should route using the resolved service endpoint (SRV + A/AAAA). Treat `lanHost`, `tailnetDns`, `gatewayPort`, and `gatewayTlsSha256` as hints only.
|
||||
- SSH auto-targeting should likewise use the resolved service host, not TXT-only hints.
|
||||
- TLS pinning must never allow an advertised `gatewayTlsSha256` to override a previously stored pin.
|
||||
- iOS/Android nodes should treat discovery-based direct connects as **TLS-only** and require explicit user confirmation before trusting a first-time fingerprint.
|
||||
|
||||
## Debugging on macOS
|
||||
|
||||
Useful built‑in tools:
|
||||
|
||||
- Browse instances:
|
||||
|
||||
```bash
|
||||
dns-sd -B _openclaw-gw._tcp local.
|
||||
```
|
||||
|
||||
- Resolve one instance (replace `<instance>`):
|
||||
|
||||
```bash
|
||||
dns-sd -L "<instance>" _openclaw-gw._tcp local.
|
||||
```
|
||||
|
||||
If browsing works but resolving fails, you’re usually hitting a LAN policy or
|
||||
mDNS resolver issue.
|
||||
|
||||
## Debugging in Gateway logs
|
||||
|
||||
The Gateway writes a rolling log file (printed on startup as
|
||||
`gateway log file: ...`). Look for `bonjour:` lines, especially:
|
||||
|
||||
- `bonjour: advertise failed ...`
|
||||
- `bonjour: ... name conflict resolved` / `hostname conflict resolved`
|
||||
- `bonjour: watchdog detected non-announced service ...`
|
||||
|
||||
## Debugging on iOS node
|
||||
|
||||
The iOS node uses `NWBrowser` to discover `_openclaw-gw._tcp`.
|
||||
|
||||
To capture logs:
|
||||
|
||||
- Settings → Gateway → Advanced → **Discovery Debug Logs**
|
||||
- Settings → Gateway → Advanced → **Discovery Logs** → reproduce → **Copy**
|
||||
|
||||
The log includes browser state transitions and result‑set changes.
|
||||
|
||||
## Common failure modes
|
||||
|
||||
- **Bonjour doesn’t cross networks**: use Tailnet or SSH.
|
||||
- **Multicast blocked**: some Wi‑Fi networks disable mDNS.
|
||||
- **Sleep / interface churn**: macOS may temporarily drop mDNS results; retry.
|
||||
- **Browse works but resolve fails**: keep machine names simple (avoid emojis or
|
||||
punctuation), then restart the Gateway. The service instance name derives from
|
||||
the host name, so overly complex names can confuse some resolvers.
|
||||
|
||||
## Escaped instance names (`\032`)
|
||||
|
||||
Bonjour/DNS‑SD often escapes bytes in service instance names as decimal `\DDD`
|
||||
sequences (e.g. spaces become `\032`).
|
||||
|
||||
- This is normal at the protocol level.
|
||||
- UIs should decode for display (iOS uses `BonjourEscapes.decode`).
|
||||
|
||||
## Disabling / configuration
|
||||
|
||||
- `OPENCLAW_DISABLE_BONJOUR=1` disables advertising (legacy: `OPENCLAW_DISABLE_BONJOUR`).
|
||||
- `gateway.bind` in `~/.openclaw/openclaw.json` controls the Gateway bind mode.
|
||||
- `OPENCLAW_SSH_PORT` overrides the SSH port when `sshPort` is advertised (legacy: `OPENCLAW_SSH_PORT`).
|
||||
- `OPENCLAW_TAILNET_DNS` publishes a MagicDNS hint in TXT (legacy: `OPENCLAW_TAILNET_DNS`).
|
||||
- `OPENCLAW_CLI_PATH` overrides the advertised CLI path (legacy: `OPENCLAW_CLI_PATH`).
|
||||
|
||||
## Related docs
|
||||
|
||||
- Discovery policy and transport selection: [Discovery](/gateway/discovery)
|
||||
- Node pairing + approvals: [Gateway pairing](/gateway/pairing)
|
||||
89
openclaw/docs/gateway/bridge-protocol.md
Normal file
89
openclaw/docs/gateway/bridge-protocol.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
---
|
||||
summary: "Historical bridge protocol (legacy nodes): TCP JSONL, pairing, scoped RPC"
|
||||
read_when:
|
||||
- Building or debugging node clients (iOS/Android/macOS node mode)
|
||||
- Investigating pairing or bridge auth failures
|
||||
- Auditing the node surface exposed by the gateway
|
||||
title: "Bridge Protocol"
|
||||
---
|
||||
|
||||
# Bridge protocol (legacy node transport)
|
||||
|
||||
<Warning>
|
||||
The TCP bridge has been **removed**. Current OpenClaw builds do not ship the bridge listener and `bridge.*` config keys are no longer in the schema. This page is kept for historical reference only. Use the [Gateway Protocol](/gateway/protocol) for all node/operator clients.
|
||||
</Warning>
|
||||
|
||||
## Why it existed
|
||||
|
||||
- **Security boundary**: the bridge exposes a small allowlist instead of the
|
||||
full gateway API surface.
|
||||
- **Pairing + node identity**: node admission is owned by the gateway and tied
|
||||
to a per-node token.
|
||||
- **Discovery UX**: nodes can discover gateways via Bonjour on LAN, or connect
|
||||
directly over a tailnet.
|
||||
- **Loopback WS**: the full WS control plane stays local unless tunneled via SSH.
|
||||
|
||||
## Transport
|
||||
|
||||
- TCP, one JSON object per line (JSONL).
|
||||
- Optional TLS (when `bridge.tls.enabled` is true).
|
||||
- Historical default listener port was `18790` (current builds do not start a
|
||||
TCP bridge).
|
||||
|
||||
When TLS is enabled, discovery TXT records include `bridgeTls=1` plus
|
||||
`bridgeTlsSha256` as a non-secret hint. Note that Bonjour/mDNS TXT records are
|
||||
unauthenticated; clients must not treat the advertised fingerprint as an
|
||||
authoritative pin without explicit user intent or other out-of-band verification.
|
||||
|
||||
## Handshake + pairing
|
||||
|
||||
1. Client sends `hello` with node metadata + token (if already paired).
|
||||
2. If not paired, gateway replies `error` (`NOT_PAIRED`/`UNAUTHORIZED`).
|
||||
3. Client sends `pair-request`.
|
||||
4. Gateway waits for approval, then sends `pair-ok` and `hello-ok`.
|
||||
|
||||
Historically, `hello-ok` returned `serverName` and could include
|
||||
`canvasHostUrl`.
|
||||
|
||||
## Frames
|
||||
|
||||
Client → Gateway:
|
||||
|
||||
- `req` / `res`: scoped gateway RPC (chat, sessions, config, health, voicewake, skills.bins)
|
||||
- `event`: node signals (voice transcript, agent request, chat subscribe, exec lifecycle)
|
||||
|
||||
Gateway → Client:
|
||||
|
||||
- `invoke` / `invoke-res`: node commands (`canvas.*`, `camera.*`, `screen.record`,
|
||||
`location.get`, `sms.send`)
|
||||
- `event`: chat updates for subscribed sessions
|
||||
- `ping` / `pong`: keepalive
|
||||
|
||||
Legacy allowlist enforcement lived in `src/gateway/server-bridge.ts` (removed).
|
||||
|
||||
## Exec lifecycle events
|
||||
|
||||
Nodes can emit `exec.finished` or `exec.denied` events to surface system.run activity.
|
||||
These are mapped to system events in the gateway. (Legacy nodes may still emit `exec.started`.)
|
||||
|
||||
Payload fields (all optional unless noted):
|
||||
|
||||
- `sessionKey` (required): agent session to receive the system event.
|
||||
- `runId`: unique exec id for grouping.
|
||||
- `command`: raw or formatted command string.
|
||||
- `exitCode`, `timedOut`, `success`, `output`: completion details (finished only).
|
||||
- `reason`: denial reason (denied only).
|
||||
|
||||
## Historical tailnet usage
|
||||
|
||||
- Bind the bridge to a tailnet IP: `bridge.bind: "tailnet"` in
|
||||
`~/.openclaw/openclaw.json` (historical only; `bridge.*` is no longer valid).
|
||||
- Clients connect via MagicDNS name or tailnet IP.
|
||||
- Bonjour does **not** cross networks; use manual host/port or wide-area DNS‑SD
|
||||
when needed.
|
||||
|
||||
## Versioning
|
||||
|
||||
The bridge was **implicit v1** (no min/max negotiation). This section is
|
||||
historical reference only; current node/operator clients use the WebSocket
|
||||
[Gateway Protocol](/gateway/protocol).
|
||||
332
openclaw/docs/gateway/cli-backends.md
Normal file
332
openclaw/docs/gateway/cli-backends.md
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
---
|
||||
summary: "CLI backends: local AI CLI fallback with optional MCP tool bridge"
|
||||
read_when:
|
||||
- You want a reliable fallback when API providers fail
|
||||
- You are running Codex CLI or other local AI CLIs and want to reuse them
|
||||
- You want to understand the MCP loopback bridge for CLI backend tool access
|
||||
title: "CLI Backends"
|
||||
---
|
||||
|
||||
# CLI backends (fallback runtime)
|
||||
|
||||
OpenClaw can run **local AI CLIs** as a **text-only fallback** when API providers are down,
|
||||
rate-limited, or temporarily misbehaving. This is intentionally conservative:
|
||||
|
||||
- **OpenClaw tools are not injected directly**, but backends with `bundleMcp: true`
|
||||
can receive gateway tools via a loopback MCP bridge.
|
||||
- **JSONL streaming** for CLIs that support it.
|
||||
- **Sessions are supported** (so follow-up turns stay coherent).
|
||||
- **Images can be passed through** if the CLI accepts image paths.
|
||||
|
||||
This is designed as a **safety net** rather than a primary path. Use it when you
|
||||
want “always works” text responses without relying on external APIs.
|
||||
|
||||
If you want a full harness runtime with ACP session controls, background tasks,
|
||||
thread/conversation binding, and persistent external coding sessions, use
|
||||
[ACP Agents](/tools/acp-agents) instead. CLI backends are not ACP.
|
||||
|
||||
## Beginner-friendly quick start
|
||||
|
||||
You can use Codex CLI **without any config** (the bundled OpenAI plugin
|
||||
registers a default backend):
|
||||
|
||||
```bash
|
||||
openclaw agent --message "hi" --model codex-cli/gpt-5.4
|
||||
```
|
||||
|
||||
If your gateway runs under launchd/systemd and PATH is minimal, add just the
|
||||
command path:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
cliBackends: {
|
||||
"codex-cli": {
|
||||
command: "/opt/homebrew/bin/codex",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
That’s it. No keys, no extra auth config needed beyond the CLI itself.
|
||||
|
||||
If you use a bundled CLI backend as the **primary message provider** on a
|
||||
gateway host, OpenClaw now auto-loads the owning bundled plugin when your config
|
||||
explicitly references that backend in a model ref or under
|
||||
`agents.defaults.cliBackends`.
|
||||
|
||||
## Using it as a fallback
|
||||
|
||||
Add a CLI backend to your fallback list so it only runs when primary models fail:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
fallbacks: ["codex-cli/gpt-5.4"],
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-opus-4-6": { alias: "Opus" },
|
||||
"codex-cli/gpt-5.4": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- If you use `agents.defaults.models` (allowlist), you must include your CLI backend models there too.
|
||||
- If the primary provider fails (auth, rate limits, timeouts), OpenClaw will
|
||||
try the CLI backend next.
|
||||
|
||||
## Configuration overview
|
||||
|
||||
All CLI backends live under:
|
||||
|
||||
```
|
||||
agents.defaults.cliBackends
|
||||
```
|
||||
|
||||
Each entry is keyed by a **provider id** (e.g. `codex-cli`, `my-cli`).
|
||||
The provider id becomes the left side of your model ref:
|
||||
|
||||
```
|
||||
<provider>/<model>
|
||||
```
|
||||
|
||||
### Example configuration
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
cliBackends: {
|
||||
"codex-cli": {
|
||||
command: "/opt/homebrew/bin/codex",
|
||||
},
|
||||
"my-cli": {
|
||||
command: "my-cli",
|
||||
args: ["--json"],
|
||||
output: "json",
|
||||
input: "arg",
|
||||
modelArg: "--model",
|
||||
modelAliases: {
|
||||
"claude-opus-4-6": "opus",
|
||||
"claude-sonnet-4-6": "sonnet",
|
||||
},
|
||||
sessionArg: "--session",
|
||||
sessionMode: "existing",
|
||||
sessionIdFields: ["session_id", "conversation_id"],
|
||||
systemPromptArg: "--system",
|
||||
// Codex-style CLIs can point at a prompt file instead:
|
||||
// systemPromptFileConfigArg: "-c",
|
||||
// systemPromptFileConfigKey: "model_instructions_file",
|
||||
systemPromptWhen: "first",
|
||||
imageArg: "--image",
|
||||
imageMode: "repeat",
|
||||
serialize: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Selects a backend** based on the provider prefix (`codex-cli/...`).
|
||||
2. **Builds a system prompt** using the same OpenClaw prompt + workspace context.
|
||||
3. **Executes the CLI** with a session id (if supported) so history stays consistent.
|
||||
4. **Parses output** (JSON or plain text) and returns the final text.
|
||||
5. **Persists session ids** per backend, so follow-ups reuse the same CLI session.
|
||||
|
||||
<Note>
|
||||
The bundled Anthropic `claude-cli` backend is supported again. Anthropic staff
|
||||
told us OpenClaw-style Claude CLI usage is allowed again, so OpenClaw treats
|
||||
`claude -p` usage as sanctioned for this integration unless Anthropic publishes
|
||||
a new policy.
|
||||
</Note>
|
||||
|
||||
The bundled OpenAI `codex-cli` backend passes OpenClaw's system prompt through
|
||||
Codex's `model_instructions_file` config override (`-c
|
||||
model_instructions_file="..."`). Codex does not expose a Claude-style
|
||||
`--append-system-prompt` flag, so OpenClaw writes the assembled prompt to a
|
||||
temporary file for each fresh Codex CLI session.
|
||||
|
||||
The bundled Anthropic `claude-cli` backend receives the OpenClaw skills snapshot
|
||||
two ways: the compact OpenClaw skills catalog in the appended system prompt, and
|
||||
a temporary Claude Code plugin passed with `--plugin-dir`. The plugin contains
|
||||
only the eligible skills for that agent/session, so Claude Code's native skill
|
||||
resolver sees the same filtered set that OpenClaw would otherwise advertise in
|
||||
the prompt. Skill env/API key overrides are still applied by OpenClaw to the
|
||||
child process environment for the run.
|
||||
|
||||
## Sessions
|
||||
|
||||
- If the CLI supports sessions, set `sessionArg` (e.g. `--session-id`) or
|
||||
`sessionArgs` (placeholder `{sessionId}`) when the ID needs to be inserted
|
||||
into multiple flags.
|
||||
- If the CLI uses a **resume subcommand** with different flags, set
|
||||
`resumeArgs` (replaces `args` when resuming) and optionally `resumeOutput`
|
||||
(for non-JSON resumes).
|
||||
- `sessionMode`:
|
||||
- `always`: always send a session id (new UUID if none stored).
|
||||
- `existing`: only send a session id if one was stored before.
|
||||
- `none`: never send a session id.
|
||||
|
||||
Serialization notes:
|
||||
|
||||
- `serialize: true` keeps same-lane runs ordered.
|
||||
- Most CLIs serialize on one provider lane.
|
||||
- OpenClaw drops stored CLI session reuse when the backend auth state changes, including relogin, token rotation, or a changed auth profile credential.
|
||||
|
||||
## Images (pass-through)
|
||||
|
||||
If your CLI accepts image paths, set `imageArg`:
|
||||
|
||||
```json5
|
||||
imageArg: "--image",
|
||||
imageMode: "repeat"
|
||||
```
|
||||
|
||||
OpenClaw will write base64 images to temp files. If `imageArg` is set, those
|
||||
paths are passed as CLI args. If `imageArg` is missing, OpenClaw appends the
|
||||
file paths to the prompt (path injection), which is enough for CLIs that auto-
|
||||
load local files from plain paths.
|
||||
|
||||
## Inputs / outputs
|
||||
|
||||
- `output: "json"` (default) tries to parse JSON and extract text + session id.
|
||||
- For Gemini CLI JSON output, OpenClaw reads reply text from `response` and
|
||||
usage from `stats` when `usage` is missing or empty.
|
||||
- `output: "jsonl"` parses JSONL streams (for example Codex CLI `--json`) and extracts the final agent message plus session
|
||||
identifiers when present.
|
||||
- `output: "text"` treats stdout as the final response.
|
||||
|
||||
Input modes:
|
||||
|
||||
- `input: "arg"` (default) passes the prompt as the last CLI arg.
|
||||
- `input: "stdin"` sends the prompt via stdin.
|
||||
- If the prompt is very long and `maxPromptArgChars` is set, stdin is used.
|
||||
|
||||
## Defaults (plugin-owned)
|
||||
|
||||
The bundled OpenAI plugin also registers a default for `codex-cli`:
|
||||
|
||||
- `command: "codex"`
|
||||
- `args: ["exec","--json","--color","never","--sandbox","workspace-write","--skip-git-repo-check"]`
|
||||
- `resumeArgs: ["exec","resume","{sessionId}","-c","sandbox_mode=\"workspace-write\"","--skip-git-repo-check"]`
|
||||
- `output: "jsonl"`
|
||||
- `resumeOutput: "text"`
|
||||
- `modelArg: "--model"`
|
||||
- `imageArg: "--image"`
|
||||
- `sessionMode: "existing"`
|
||||
|
||||
The bundled Google plugin also registers a default for `google-gemini-cli`:
|
||||
|
||||
- `command: "gemini"`
|
||||
- `args: ["--output-format", "json", "--prompt", "{prompt}"]`
|
||||
- `resumeArgs: ["--resume", "{sessionId}", "--output-format", "json", "--prompt", "{prompt}"]`
|
||||
- `imageArg: "@"`
|
||||
- `imagePathScope: "workspace"`
|
||||
- `modelArg: "--model"`
|
||||
- `sessionMode: "existing"`
|
||||
- `sessionIdFields: ["session_id", "sessionId"]`
|
||||
|
||||
Prerequisite: the local Gemini CLI must be installed and available as
|
||||
`gemini` on `PATH` (`brew install gemini-cli` or
|
||||
`npm install -g @google/gemini-cli`).
|
||||
|
||||
Gemini CLI JSON notes:
|
||||
|
||||
- Reply text is read from the JSON `response` field.
|
||||
- Usage falls back to `stats` when `usage` is absent or empty.
|
||||
- `stats.cached` is normalized into OpenClaw `cacheRead`.
|
||||
- If `stats.input` is missing, OpenClaw derives input tokens from
|
||||
`stats.input_tokens - stats.cached`.
|
||||
|
||||
Override only if needed (common: absolute `command` path).
|
||||
|
||||
## Plugin-owned defaults
|
||||
|
||||
CLI backend defaults are now part of the plugin surface:
|
||||
|
||||
- Plugins register them with `api.registerCliBackend(...)`.
|
||||
- The backend `id` becomes the provider prefix in model refs.
|
||||
- User config in `agents.defaults.cliBackends.<id>` still overrides the plugin default.
|
||||
- Backend-specific config cleanup stays plugin-owned through the optional
|
||||
`normalizeConfig` hook.
|
||||
|
||||
Plugins that need tiny prompt/message compatibility shims can declare
|
||||
bidirectional text transforms without replacing a provider or CLI backend:
|
||||
|
||||
```typescript
|
||||
api.registerTextTransforms({
|
||||
input: [
|
||||
{ from: /red basket/g, to: "blue basket" },
|
||||
{ from: /paper ticket/g, to: "digital ticket" },
|
||||
{ from: /left shelf/g, to: "right shelf" },
|
||||
],
|
||||
output: [
|
||||
{ from: /blue basket/g, to: "red basket" },
|
||||
{ from: /digital ticket/g, to: "paper ticket" },
|
||||
{ from: /right shelf/g, to: "left shelf" },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
`input` rewrites the system prompt and user prompt passed to the CLI. `output`
|
||||
rewrites streamed assistant deltas and parsed final text before OpenClaw handles
|
||||
its own control markers and channel delivery.
|
||||
|
||||
For CLIs that emit Claude Code stream-json compatible JSONL, set
|
||||
`jsonlDialect: "claude-stream-json"` on that backend's config.
|
||||
|
||||
## Bundle MCP overlays
|
||||
|
||||
CLI backends do **not** receive OpenClaw tool calls directly, but a backend can
|
||||
opt into a generated MCP config overlay with `bundleMcp: true`.
|
||||
|
||||
Current bundled behavior:
|
||||
|
||||
- `claude-cli`: generated strict MCP config file
|
||||
- `codex-cli`: inline config overrides for `mcp_servers`
|
||||
- `google-gemini-cli`: generated Gemini system settings file
|
||||
|
||||
When bundle MCP is enabled, OpenClaw:
|
||||
|
||||
- spawns a loopback HTTP MCP server that exposes gateway tools to the CLI process
|
||||
- authenticates the bridge with a per-session token (`OPENCLAW_MCP_TOKEN`)
|
||||
- scopes tool access to the current session, account, and channel context
|
||||
- loads enabled bundle-MCP servers for the current workspace
|
||||
- merges them with any existing backend MCP config/settings shape
|
||||
- rewrites the launch config using the backend-owned integration mode from the owning extension
|
||||
|
||||
If no MCP servers are enabled, OpenClaw still injects a strict config when a
|
||||
backend opts into bundle MCP so background runs stay isolated.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **No direct OpenClaw tool calls.** OpenClaw does not inject tool calls into
|
||||
the CLI backend protocol. Backends only see gateway tools when they opt into
|
||||
`bundleMcp: true`.
|
||||
- **Streaming is backend-specific.** Some backends stream JSONL; others buffer
|
||||
until exit.
|
||||
- **Structured outputs** depend on the CLI’s JSON format.
|
||||
- **Codex CLI sessions** resume via text output (no JSONL), which is less
|
||||
structured than the initial `--json` run. OpenClaw sessions still work
|
||||
normally.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **CLI not found**: set `command` to a full path.
|
||||
- **Wrong model name**: use `modelAliases` to map `provider/model` → CLI model.
|
||||
- **No session continuity**: ensure `sessionArg` is set and `sessionMode` is not
|
||||
`none` (Codex CLI currently cannot resume with JSON output).
|
||||
- **Images ignored**: set `imageArg` (and verify CLI supports file paths).
|
||||
631
openclaw/docs/gateway/configuration-examples.md
Normal file
631
openclaw/docs/gateway/configuration-examples.md
Normal file
|
|
@ -0,0 +1,631 @@
|
|||
---
|
||||
summary: "Schema-accurate configuration examples for common OpenClaw setups"
|
||||
read_when:
|
||||
- Learning how to configure OpenClaw
|
||||
- Looking for configuration examples
|
||||
- Setting up OpenClaw for the first time
|
||||
title: "Configuration Examples"
|
||||
---
|
||||
|
||||
# Configuration Examples
|
||||
|
||||
Examples below are aligned with the current config schema. For the exhaustive reference and per-field notes, see [Configuration](/gateway/configuration).
|
||||
|
||||
## Quick start
|
||||
|
||||
### Absolute minimum
|
||||
|
||||
```json5
|
||||
{
|
||||
agent: { workspace: "~/.openclaw/workspace" },
|
||||
channels: { whatsapp: { allowFrom: ["+15555550123"] } },
|
||||
}
|
||||
```
|
||||
|
||||
Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
|
||||
|
||||
### Recommended starter
|
||||
|
||||
```json5
|
||||
{
|
||||
identity: {
|
||||
name: "Clawd",
|
||||
theme: "helpful assistant",
|
||||
emoji: "🦞",
|
||||
},
|
||||
agent: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
model: { primary: "anthropic/claude-sonnet-4-6" },
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Expanded example (major options)
|
||||
|
||||
> JSON5 lets you use comments and trailing commas. Regular JSON works too.
|
||||
|
||||
```json5
|
||||
{
|
||||
// Environment + shell
|
||||
env: {
|
||||
OPENROUTER_API_KEY: "sk-or-...",
|
||||
vars: {
|
||||
GROQ_API_KEY: "gsk-...",
|
||||
},
|
||||
shellEnv: {
|
||||
enabled: true,
|
||||
timeoutMs: 15000,
|
||||
},
|
||||
},
|
||||
|
||||
// Auth profile metadata (secrets live in auth-profiles.json)
|
||||
auth: {
|
||||
profiles: {
|
||||
"anthropic:default": { provider: "anthropic", mode: "api_key" },
|
||||
"anthropic:work": { provider: "anthropic", mode: "api_key" },
|
||||
"openai:default": { provider: "openai", mode: "api_key" },
|
||||
"openai-codex:personal": { provider: "openai-codex", mode: "oauth" },
|
||||
},
|
||||
order: {
|
||||
anthropic: ["anthropic:default", "anthropic:work"],
|
||||
openai: ["openai:default"],
|
||||
"openai-codex": ["openai-codex:personal"],
|
||||
},
|
||||
},
|
||||
|
||||
// Identity
|
||||
identity: {
|
||||
name: "Samantha",
|
||||
theme: "helpful sloth",
|
||||
emoji: "🦥",
|
||||
},
|
||||
|
||||
// Logging
|
||||
logging: {
|
||||
level: "info",
|
||||
file: "/tmp/openclaw/openclaw.log",
|
||||
consoleLevel: "info",
|
||||
consoleStyle: "pretty",
|
||||
redactSensitive: "tools",
|
||||
},
|
||||
|
||||
// Message formatting
|
||||
messages: {
|
||||
messagePrefix: "[openclaw]",
|
||||
responsePrefix: ">",
|
||||
ackReaction: "👀",
|
||||
ackReactionScope: "group-mentions",
|
||||
},
|
||||
|
||||
// Routing + queue
|
||||
routing: {
|
||||
groupChat: {
|
||||
mentionPatterns: ["@openclaw", "openclaw"],
|
||||
historyLimit: 50,
|
||||
},
|
||||
queue: {
|
||||
mode: "collect",
|
||||
debounceMs: 1000,
|
||||
cap: 20,
|
||||
drop: "summarize",
|
||||
byChannel: {
|
||||
whatsapp: "collect",
|
||||
telegram: "collect",
|
||||
discord: "collect",
|
||||
slack: "collect",
|
||||
signal: "collect",
|
||||
imessage: "collect",
|
||||
webchat: "collect",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Tooling
|
||||
tools: {
|
||||
media: {
|
||||
audio: {
|
||||
enabled: true,
|
||||
maxBytes: 20971520,
|
||||
models: [
|
||||
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
|
||||
// Optional CLI fallback (Whisper binary):
|
||||
// { type: "cli", command: "whisper", args: ["--model", "base", "{{MediaPath}}"] }
|
||||
],
|
||||
timeoutSeconds: 120,
|
||||
},
|
||||
video: {
|
||||
enabled: true,
|
||||
maxBytes: 52428800,
|
||||
models: [{ provider: "google", model: "gemini-3-flash-preview" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Session behavior
|
||||
session: {
|
||||
scope: "per-sender",
|
||||
dmScope: "per-channel-peer", // recommended for multi-user inboxes
|
||||
reset: {
|
||||
mode: "daily",
|
||||
atHour: 4,
|
||||
idleMinutes: 60,
|
||||
},
|
||||
resetByChannel: {
|
||||
discord: { mode: "idle", idleMinutes: 10080 },
|
||||
},
|
||||
resetTriggers: ["/new", "/reset"],
|
||||
store: "~/.openclaw/agents/default/sessions/sessions.json",
|
||||
maintenance: {
|
||||
mode: "warn",
|
||||
pruneAfter: "30d",
|
||||
maxEntries: 500,
|
||||
rotateBytes: "10mb",
|
||||
resetArchiveRetention: "30d", // duration or false
|
||||
maxDiskBytes: "500mb", // optional
|
||||
highWaterBytes: "400mb", // optional (defaults to 80% of maxDiskBytes)
|
||||
},
|
||||
typingIntervalSeconds: 5,
|
||||
sendPolicy: {
|
||||
default: "allow",
|
||||
rules: [{ action: "deny", match: { channel: "discord", chatType: "group" } }],
|
||||
},
|
||||
},
|
||||
|
||||
// Channels
|
||||
channels: {
|
||||
whatsapp: {
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["+15555550123"],
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123"],
|
||||
groups: { "*": { requireMention: true } },
|
||||
},
|
||||
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "YOUR_TELEGRAM_BOT_TOKEN",
|
||||
allowFrom: ["123456789"],
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["123456789"],
|
||||
groups: { "*": { requireMention: true } },
|
||||
},
|
||||
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "YOUR_DISCORD_BOT_TOKEN",
|
||||
dm: { enabled: true, allowFrom: ["123456789012345678"] },
|
||||
guilds: {
|
||||
"123456789012345678": {
|
||||
slug: "friends-of-openclaw",
|
||||
requireMention: false,
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
help: { allow: true, requireMention: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
slack: {
|
||||
enabled: true,
|
||||
botToken: "xoxb-REPLACE_ME",
|
||||
appToken: "xapp-REPLACE_ME",
|
||||
channels: {
|
||||
"#general": { allow: true, requireMention: true },
|
||||
},
|
||||
dm: { enabled: true, allowFrom: ["U123"] },
|
||||
slashCommand: {
|
||||
enabled: true,
|
||||
name: "openclaw",
|
||||
sessionPrefix: "slack:slash",
|
||||
ephemeral: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Agent runtime
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
userTimezone: "America/Chicago",
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4-6",
|
||||
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.4"],
|
||||
},
|
||||
imageModel: {
|
||||
primary: "openrouter/anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-opus-4-6": { alias: "opus" },
|
||||
"anthropic/claude-sonnet-4-6": { alias: "sonnet" },
|
||||
"openai/gpt-5.4": { alias: "gpt" },
|
||||
},
|
||||
skills: ["github", "weather"], // inherited by agents that omit list[].skills
|
||||
thinkingDefault: "low",
|
||||
verboseDefault: "off",
|
||||
elevatedDefault: "on",
|
||||
blockStreamingDefault: "off",
|
||||
blockStreamingBreak: "text_end",
|
||||
blockStreamingChunk: {
|
||||
minChars: 800,
|
||||
maxChars: 1200,
|
||||
breakPreference: "paragraph",
|
||||
},
|
||||
blockStreamingCoalesce: {
|
||||
idleMs: 1000,
|
||||
},
|
||||
humanDelay: {
|
||||
mode: "natural",
|
||||
},
|
||||
timeoutSeconds: 600,
|
||||
mediaMaxMb: 5,
|
||||
typingIntervalSeconds: 5,
|
||||
maxConcurrent: 3,
|
||||
heartbeat: {
|
||||
every: "30m",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
target: "last",
|
||||
directPolicy: "allow", // allow (default) | block
|
||||
to: "+15555550123",
|
||||
prompt: "HEARTBEAT",
|
||||
ackMaxChars: 300,
|
||||
},
|
||||
memorySearch: {
|
||||
provider: "gemini",
|
||||
model: "gemini-embedding-001",
|
||||
remote: {
|
||||
apiKey: "${GEMINI_API_KEY}",
|
||||
},
|
||||
extraPaths: ["../team-docs", "/srv/shared-notes"],
|
||||
},
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
scope: "session", // preferred over legacy perSession: true
|
||||
workspaceRoot: "~/.openclaw/sandboxes",
|
||||
docker: {
|
||||
image: "openclaw-sandbox:bookworm-slim",
|
||||
workdir: "/workspace",
|
||||
readOnlyRoot: true,
|
||||
tmpfs: ["/tmp", "/var/tmp", "/run"],
|
||||
network: "none",
|
||||
user: "1000:1000",
|
||||
},
|
||||
browser: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
default: true,
|
||||
// inherits defaults.skills -> github, weather
|
||||
thinkingDefault: "high", // per-agent thinking override
|
||||
reasoningDefault: "on", // per-agent reasoning visibility
|
||||
fastModeDefault: false, // per-agent fast mode
|
||||
},
|
||||
{
|
||||
id: "quick",
|
||||
skills: [], // no skills for this agent
|
||||
fastModeDefault: true, // this agent always runs fast
|
||||
thinkingDefault: "off",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
tools: {
|
||||
allow: ["exec", "process", "read", "write", "edit", "apply_patch"],
|
||||
deny: ["browser", "canvas"],
|
||||
exec: {
|
||||
backgroundMs: 10000,
|
||||
timeoutSec: 1800,
|
||||
cleanupMs: 1800000,
|
||||
},
|
||||
elevated: {
|
||||
enabled: true,
|
||||
allowFrom: {
|
||||
whatsapp: ["+15555550123"],
|
||||
telegram: ["123456789"],
|
||||
discord: ["123456789012345678"],
|
||||
slack: ["U123"],
|
||||
signal: ["+15555550123"],
|
||||
imessage: ["user@example.com"],
|
||||
webchat: ["session:demo"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Custom model providers
|
||||
models: {
|
||||
mode: "merge",
|
||||
providers: {
|
||||
"custom-proxy": {
|
||||
baseUrl: "http://localhost:4000/v1",
|
||||
apiKey: "LITELLM_KEY",
|
||||
api: "openai-responses",
|
||||
authHeader: true,
|
||||
headers: { "X-Proxy-Region": "us-west" },
|
||||
models: [
|
||||
{
|
||||
id: "llama-3.1-8b",
|
||||
name: "Llama 3.1 8B",
|
||||
api: "openai-responses",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Cron jobs
|
||||
cron: {
|
||||
enabled: true,
|
||||
store: "~/.openclaw/cron/cron.json",
|
||||
maxConcurrentRuns: 2,
|
||||
sessionRetention: "24h",
|
||||
runLog: {
|
||||
maxBytes: "2mb",
|
||||
keepLines: 2000,
|
||||
},
|
||||
},
|
||||
|
||||
// Webhooks
|
||||
hooks: {
|
||||
enabled: true,
|
||||
path: "/hooks",
|
||||
token: "shared-secret",
|
||||
presets: ["gmail"],
|
||||
transformsDir: "~/.openclaw/hooks/transforms",
|
||||
mappings: [
|
||||
{
|
||||
id: "gmail-hook",
|
||||
match: { path: "gmail" },
|
||||
action: "agent",
|
||||
wakeMode: "now",
|
||||
name: "Gmail",
|
||||
sessionKey: "hook:gmail:{{messages[0].id}}",
|
||||
messageTemplate: "From: {{messages[0].from}}\nSubject: {{messages[0].subject}}",
|
||||
textTemplate: "{{messages[0].snippet}}",
|
||||
deliver: true,
|
||||
channel: "last",
|
||||
to: "+15555550123",
|
||||
thinking: "low",
|
||||
timeoutSeconds: 300,
|
||||
transform: {
|
||||
module: "gmail.js",
|
||||
export: "transformGmail",
|
||||
},
|
||||
},
|
||||
],
|
||||
gmail: {
|
||||
account: "openclaw@gmail.com",
|
||||
label: "INBOX",
|
||||
topic: "projects/<project-id>/topics/gog-gmail-watch",
|
||||
subscription: "gog-gmail-watch-push",
|
||||
pushToken: "shared-push-token",
|
||||
hookUrl: "http://127.0.0.1:18789/hooks/gmail",
|
||||
includeBody: true,
|
||||
maxBytes: 20000,
|
||||
renewEveryMinutes: 720,
|
||||
serve: { bind: "127.0.0.1", port: 8788, path: "/" },
|
||||
tailscale: { mode: "funnel", path: "/gmail-pubsub" },
|
||||
},
|
||||
},
|
||||
|
||||
// Gateway + networking
|
||||
gateway: {
|
||||
mode: "local",
|
||||
port: 18789,
|
||||
bind: "loopback",
|
||||
controlUi: { enabled: true, basePath: "/openclaw" },
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "gateway-token",
|
||||
allowTailscale: true,
|
||||
},
|
||||
tailscale: { mode: "serve", resetOnExit: false },
|
||||
remote: { url: "ws://gateway.tailnet:18789", token: "remote-token" },
|
||||
reload: { mode: "hybrid", debounceMs: 300 },
|
||||
},
|
||||
|
||||
skills: {
|
||||
allowBundled: ["gemini", "peekaboo"],
|
||||
load: {
|
||||
extraDirs: ["~/Projects/agent-scripts/skills"],
|
||||
},
|
||||
install: {
|
||||
preferBrew: true,
|
||||
nodeManager: "npm", // npm | pnpm | yarn | bun
|
||||
},
|
||||
entries: {
|
||||
"image-lab": {
|
||||
enabled: true,
|
||||
apiKey: "GEMINI_KEY_HERE",
|
||||
env: { GEMINI_API_KEY: "GEMINI_KEY_HERE" },
|
||||
},
|
||||
peekaboo: { enabled: true },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Common patterns
|
||||
|
||||
### Shared skill baseline with one override
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
skills: ["github", "weather"],
|
||||
},
|
||||
list: [
|
||||
{ id: "main", default: true },
|
||||
{ id: "docs", workspace: "~/.openclaw/workspace-docs", skills: ["docs-search"] },
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `agents.defaults.skills` is the shared baseline.
|
||||
- `agents.list[].skills` replaces that baseline for one agent.
|
||||
- Use `skills: []` when an agent should see no skills.
|
||||
|
||||
### Multi-platform setup
|
||||
|
||||
```json5
|
||||
{
|
||||
agent: { workspace: "~/.openclaw/workspace" },
|
||||
channels: {
|
||||
whatsapp: { allowFrom: ["+15555550123"] },
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "YOUR_TOKEN",
|
||||
allowFrom: ["123456789"],
|
||||
},
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "YOUR_TOKEN",
|
||||
dm: { allowFrom: ["123456789012345678"] },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Secure DM mode (shared inbox / multi-user DMs)
|
||||
|
||||
If more than one person can DM your bot (multiple entries in `allowFrom`, pairing approvals for multiple people, or `dmPolicy: "open"`), enable **secure DM mode** so DMs from different senders don’t share one context by default:
|
||||
|
||||
```json5
|
||||
{
|
||||
// Secure DM mode (recommended for multi-user or sensitive DM agents)
|
||||
session: { dmScope: "per-channel-peer" },
|
||||
|
||||
channels: {
|
||||
// Example: WhatsApp multi-user inbox
|
||||
whatsapp: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["+15555550123", "+15555550124"],
|
||||
},
|
||||
|
||||
// Example: Discord multi-user inbox
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "YOUR_DISCORD_BOT_TOKEN",
|
||||
dm: { enabled: true, allowFrom: ["123456789012345678", "987654321098765432"] },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
For Discord/Slack/Google Chat/Microsoft Teams/Mattermost/IRC, sender authorization is ID-first by default.
|
||||
Only enable direct mutable name/email/nick matching with each channel's `dangerouslyAllowNameMatching: true` if you explicitly accept that risk.
|
||||
|
||||
### Anthropic API key + MiniMax fallback
|
||||
|
||||
```json5
|
||||
{
|
||||
auth: {
|
||||
profiles: {
|
||||
"anthropic:api": {
|
||||
provider: "anthropic",
|
||||
mode: "api_key",
|
||||
},
|
||||
},
|
||||
order: {
|
||||
anthropic: ["anthropic:api"],
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
minimax: {
|
||||
baseUrl: "https://api.minimax.io/anthropic",
|
||||
api: "anthropic-messages",
|
||||
apiKey: "${MINIMAX_API_KEY}",
|
||||
},
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
fallbacks: ["minimax/MiniMax-M2.7"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Work bot (restricted access)
|
||||
|
||||
```json5
|
||||
{
|
||||
identity: {
|
||||
name: "WorkBot",
|
||||
theme: "professional assistant",
|
||||
},
|
||||
agent: {
|
||||
workspace: "~/work-openclaw",
|
||||
elevated: { enabled: false },
|
||||
},
|
||||
channels: {
|
||||
slack: {
|
||||
enabled: true,
|
||||
botToken: "xoxb-...",
|
||||
channels: {
|
||||
"#engineering": { allow: true, requireMention: true },
|
||||
"#general": { allow: true, requireMention: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Local models only
|
||||
|
||||
```json5
|
||||
{
|
||||
agent: {
|
||||
workspace: "~/.openclaw/workspace",
|
||||
model: { primary: "lmstudio/my-local-model" },
|
||||
},
|
||||
models: {
|
||||
mode: "merge",
|
||||
providers: {
|
||||
lmstudio: {
|
||||
baseUrl: "http://127.0.0.1:1234/v1",
|
||||
apiKey: "lmstudio",
|
||||
api: "openai-responses",
|
||||
models: [
|
||||
{
|
||||
id: "my-local-model",
|
||||
name: "Local Model",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 196608,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- If you set `dmPolicy: "open"`, the matching `allowFrom` list must include `"*"`.
|
||||
- Provider IDs differ (phone numbers, user IDs, channel IDs). Use the provider docs to confirm the format.
|
||||
- Optional sections to add later: `web`, `browser`, `ui`, `discovery`, `canvasHost`, `talk`, `signal`, `imessage`.
|
||||
- See [Providers](/providers) and [Troubleshooting](/gateway/troubleshooting) for deeper setup notes.
|
||||
3879
openclaw/docs/gateway/configuration-reference.md
Normal file
3879
openclaw/docs/gateway/configuration-reference.md
Normal file
File diff suppressed because it is too large
Load diff
726
openclaw/docs/gateway/configuration.md
Normal file
726
openclaw/docs/gateway/configuration.md
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
---
|
||||
summary: "Configuration overview: common tasks, quick setup, and links to the full reference"
|
||||
read_when:
|
||||
- Setting up OpenClaw for the first time
|
||||
- Looking for common configuration patterns
|
||||
- Navigating to specific config sections
|
||||
title: "Configuration"
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
OpenClaw reads an optional <Tooltip tip="JSON5 supports comments and trailing commas">**JSON5**</Tooltip> config from `~/.openclaw/openclaw.json`.
|
||||
|
||||
If the file is missing, OpenClaw uses safe defaults. Common reasons to add a config:
|
||||
|
||||
- Connect channels and control who can message the bot
|
||||
- Set models, tools, sandboxing, or automation (cron, hooks)
|
||||
- Tune sessions, media, networking, or UI
|
||||
|
||||
See the [full reference](/gateway/configuration-reference) for every available field.
|
||||
|
||||
<Tip>
|
||||
**New to configuration?** Start with `openclaw onboard` for interactive setup, or check out the [Configuration Examples](/gateway/configuration-examples) guide for complete copy-paste configs.
|
||||
</Tip>
|
||||
|
||||
## Minimal config
|
||||
|
||||
```json5
|
||||
// ~/.openclaw/openclaw.json
|
||||
{
|
||||
agents: { defaults: { workspace: "~/.openclaw/workspace" } },
|
||||
channels: { whatsapp: { allowFrom: ["+15555550123"] } },
|
||||
}
|
||||
```
|
||||
|
||||
## Editing config
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Interactive wizard">
|
||||
```bash
|
||||
openclaw onboard # full onboarding flow
|
||||
openclaw configure # config wizard
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="CLI (one-liners)">
|
||||
```bash
|
||||
openclaw config get agents.defaults.workspace
|
||||
openclaw config set agents.defaults.heartbeat.every "2h"
|
||||
openclaw config unset plugins.entries.brave.config.webSearch.apiKey
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Control UI">
|
||||
Open [http://127.0.0.1:18789](http://127.0.0.1:18789) and use the **Config** tab.
|
||||
The Control UI renders a form from the live config schema, including field
|
||||
`title` / `description` docs metadata plus plugin and channel schemas when
|
||||
available, with a **Raw JSON** editor as an escape hatch. For drill-down
|
||||
UIs and other tooling, the gateway also exposes `config.schema.lookup` to
|
||||
fetch one path-scoped schema node plus immediate child summaries.
|
||||
</Tab>
|
||||
<Tab title="Direct edit">
|
||||
Edit `~/.openclaw/openclaw.json` directly. The Gateway watches the file and applies changes automatically (see [hot reload](#config-hot-reload)).
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Strict validation
|
||||
|
||||
<Warning>
|
||||
OpenClaw only accepts configurations that fully match the schema. Unknown keys, malformed types, or invalid values cause the Gateway to **refuse to start**. The only root-level exception is `$schema` (string), so editors can attach JSON Schema metadata.
|
||||
</Warning>
|
||||
|
||||
Schema tooling notes:
|
||||
|
||||
- `openclaw config schema` prints the same JSON Schema family used by Control UI
|
||||
and config validation.
|
||||
- Treat that schema output as the canonical machine-readable contract for
|
||||
`openclaw.json`; this overview and the configuration reference summarize it.
|
||||
- Field `title` and `description` values are carried into the schema output for
|
||||
editor and form tooling.
|
||||
- Nested object, wildcard (`*`), and array-item (`[]`) entries inherit the same
|
||||
docs metadata where matching field documentation exists.
|
||||
- `anyOf` / `oneOf` / `allOf` composition branches inherit the same docs
|
||||
metadata too, so union/intersection variants keep the same field help.
|
||||
- `config.schema.lookup` returns one normalized config path with a shallow
|
||||
schema node (`title`, `description`, `type`, `enum`, `const`, common bounds,
|
||||
and similar validation fields), matched UI hint metadata, and immediate child
|
||||
summaries for drill-down tooling.
|
||||
- Runtime plugin/channel schemas are merged in when the gateway can load the
|
||||
current manifest registry.
|
||||
- `pnpm config:docs:check` detects drift between docs-facing config baseline
|
||||
artifacts and the current schema surface.
|
||||
|
||||
When validation fails:
|
||||
|
||||
- The Gateway does not boot
|
||||
- Only diagnostic commands work (`openclaw doctor`, `openclaw logs`, `openclaw health`, `openclaw status`)
|
||||
- Run `openclaw doctor` to see exact issues
|
||||
- Run `openclaw doctor --fix` (or `--yes`) to apply repairs
|
||||
|
||||
The Gateway also keeps a trusted last-known-good copy after a successful startup. If
|
||||
`openclaw.json` is later changed outside OpenClaw and no longer validates, startup
|
||||
and hot reload preserve the broken file as a timestamped `.clobbered.*` snapshot,
|
||||
restore the last-known-good copy, and log a loud warning with the recovery reason.
|
||||
The next main-agent turn also receives a system-event warning telling it that the
|
||||
config was restored and must not be blindly rewritten. Last-known-good promotion
|
||||
is updated after validated startup and after accepted hot reloads, including
|
||||
OpenClaw-owned config writes whose persisted file hash still matches the accepted
|
||||
write. Promotion is skipped when the candidate contains redacted secret
|
||||
placeholders such as `***` or shortened token values.
|
||||
|
||||
## Common tasks
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Set up a channel (WhatsApp, Telegram, Discord, etc.)">
|
||||
Each channel has its own config section under `channels.<provider>`. See the dedicated channel page for setup steps:
|
||||
|
||||
- [WhatsApp](/channels/whatsapp) — `channels.whatsapp`
|
||||
- [Telegram](/channels/telegram) — `channels.telegram`
|
||||
- [Discord](/channels/discord) — `channels.discord`
|
||||
- [Feishu](/channels/feishu) — `channels.feishu`
|
||||
- [Google Chat](/channels/googlechat) — `channels.googlechat`
|
||||
- [Microsoft Teams](/channels/msteams) — `channels.msteams`
|
||||
- [Slack](/channels/slack) — `channels.slack`
|
||||
- [Signal](/channels/signal) — `channels.signal`
|
||||
- [iMessage](/channels/imessage) — `channels.imessage`
|
||||
- [Mattermost](/channels/mattermost) — `channels.mattermost`
|
||||
|
||||
All channels share the same DM policy pattern:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "123:abc",
|
||||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||||
allowFrom: ["tg:123"], // only for allowlist/open
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Choose and configure models">
|
||||
Set the primary model and optional fallbacks:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4-6",
|
||||
fallbacks: ["openai/gpt-5.4"],
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||
"openai/gpt-5.4": { alias: "GPT" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `agents.defaults.models` defines the model catalog and acts as the allowlist for `/model`.
|
||||
- Model refs use `provider/model` format (e.g. `anthropic/claude-opus-4-6`).
|
||||
- `agents.defaults.imageMaxDimensionPx` controls transcript/tool image downscaling (default `1200`); lower values usually reduce vision-token usage on screenshot-heavy runs.
|
||||
- See [Models CLI](/concepts/models) for switching models in chat and [Model Failover](/concepts/model-failover) for auth rotation and fallback behavior.
|
||||
- For custom/self-hosted providers, see [Custom providers](/gateway/configuration-reference#custom-providers-and-base-urls) in the reference.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Control who can message the bot">
|
||||
DM access is controlled per channel via `dmPolicy`:
|
||||
|
||||
- `"pairing"` (default): unknown senders get a one-time pairing code to approve
|
||||
- `"allowlist"`: only senders in `allowFrom` (or the paired allow store)
|
||||
- `"open"`: allow all inbound DMs (requires `allowFrom: ["*"]`)
|
||||
- `"disabled"`: ignore all DMs
|
||||
|
||||
For groups, use `groupPolicy` + `groupAllowFrom` or channel-specific allowlists.
|
||||
|
||||
See the [full reference](/gateway/configuration-reference#dm-and-group-access) for per-channel details.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Set up group chat mention gating">
|
||||
Group messages default to **require mention**. Configure patterns per agent:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
groupChat: {
|
||||
mentionPatterns: ["@openclaw", "openclaw"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: { "*": { requireMention: true } },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- **Metadata mentions**: native @-mentions (WhatsApp tap-to-mention, Telegram @bot, etc.)
|
||||
- **Text patterns**: safe regex patterns in `mentionPatterns`
|
||||
- See [full reference](/gateway/configuration-reference#group-chat-mention-gating) for per-channel overrides and self-chat mode.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Restrict skills per agent">
|
||||
Use `agents.defaults.skills` for a shared baseline, then override specific
|
||||
agents with `agents.list[].skills`:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
skills: ["github", "weather"],
|
||||
},
|
||||
list: [
|
||||
{ id: "writer" }, // inherits github, weather
|
||||
{ id: "docs", skills: ["docs-search"] }, // replaces defaults
|
||||
{ id: "locked-down", skills: [] }, // no skills
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- Omit `agents.defaults.skills` for unrestricted skills by default.
|
||||
- Omit `agents.list[].skills` to inherit the defaults.
|
||||
- Set `agents.list[].skills: []` for no skills.
|
||||
- See [Skills](/tools/skills), [Skills config](/tools/skills-config), and
|
||||
the [Configuration Reference](/gateway/configuration-reference#agents-defaults-skills).
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Tune gateway channel health monitoring">
|
||||
Control how aggressively the gateway restarts channels that look stale:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
channelHealthCheckMinutes: 5,
|
||||
channelStaleEventThresholdMinutes: 30,
|
||||
channelMaxRestartsPerHour: 10,
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
healthMonitor: { enabled: false },
|
||||
accounts: {
|
||||
alerts: {
|
||||
healthMonitor: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- Set `gateway.channelHealthCheckMinutes: 0` to disable health-monitor restarts globally.
|
||||
- `channelStaleEventThresholdMinutes` should be greater than or equal to the check interval.
|
||||
- Use `channels.<provider>.healthMonitor.enabled` or `channels.<provider>.accounts.<id>.healthMonitor.enabled` to disable auto-restarts for one channel or account without disabling the global monitor.
|
||||
- See [Health Checks](/gateway/health) for operational debugging and the [full reference](/gateway/configuration-reference#gateway) for all fields.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Configure sessions and resets">
|
||||
Sessions control conversation continuity and isolation:
|
||||
|
||||
```json5
|
||||
{
|
||||
session: {
|
||||
dmScope: "per-channel-peer", // recommended for multi-user
|
||||
threadBindings: {
|
||||
enabled: true,
|
||||
idleHours: 24,
|
||||
maxAgeHours: 0,
|
||||
},
|
||||
reset: {
|
||||
mode: "daily",
|
||||
atHour: 4,
|
||||
idleMinutes: 120,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `dmScope`: `main` (shared) | `per-peer` | `per-channel-peer` | `per-account-channel-peer`
|
||||
- `threadBindings`: global defaults for thread-bound session routing (Discord supports `/focus`, `/unfocus`, `/agents`, `/session idle`, and `/session max-age`).
|
||||
- See [Session Management](/concepts/session) for scoping, identity links, and send policy.
|
||||
- See [full reference](/gateway/configuration-reference#session) for all fields.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Enable sandboxing">
|
||||
Run agent sessions in isolated Docker containers:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "non-main", // off | non-main | all
|
||||
scope: "agent", // session | agent | shared
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Build the image first: `scripts/sandbox-setup.sh`
|
||||
|
||||
See [Sandboxing](/gateway/sandboxing) for the full guide and [full reference](/gateway/configuration-reference#agentsdefaultssandbox) for all options.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Enable relay-backed push for official iOS builds">
|
||||
Relay-backed push is configured in `openclaw.json`.
|
||||
|
||||
Set this in gateway config:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com",
|
||||
// Optional. Default: 10000
|
||||
timeoutMs: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
CLI equivalent:
|
||||
|
||||
```bash
|
||||
openclaw config set gateway.push.apns.relay.baseUrl https://relay.example.com
|
||||
```
|
||||
|
||||
What this does:
|
||||
|
||||
- Lets the gateway send `push.test`, wake nudges, and reconnect wakes through the external relay.
|
||||
- Uses a registration-scoped send grant forwarded by the paired iOS app. The gateway does not need a deployment-wide relay token.
|
||||
- Binds each relay-backed registration to the gateway identity that the iOS app paired with, so another gateway cannot reuse the stored registration.
|
||||
- Keeps local/manual iOS builds on direct APNs. Relay-backed sends apply only to official distributed builds that registered through the relay.
|
||||
- Must match the relay base URL baked into the official/TestFlight iOS build, so registration and send traffic reach the same relay deployment.
|
||||
|
||||
End-to-end flow:
|
||||
|
||||
1. Install an official/TestFlight iOS build that was compiled with the same relay base URL.
|
||||
2. Configure `gateway.push.apns.relay.baseUrl` on the gateway.
|
||||
3. Pair the iOS app to the gateway and let both node and operator sessions connect.
|
||||
4. The iOS app fetches the gateway identity, registers with the relay using App Attest plus the app receipt, and then publishes the relay-backed `push.apns.register` payload to the paired gateway.
|
||||
5. The gateway stores the relay handle and send grant, then uses them for `push.test`, wake nudges, and reconnect wakes.
|
||||
|
||||
Operational notes:
|
||||
|
||||
- If you switch the iOS app to a different gateway, reconnect the app so it can publish a new relay registration bound to that gateway.
|
||||
- If you ship a new iOS build that points at a different relay deployment, the app refreshes its cached relay registration instead of reusing the old relay origin.
|
||||
|
||||
Compatibility note:
|
||||
|
||||
- `OPENCLAW_APNS_RELAY_BASE_URL` and `OPENCLAW_APNS_RELAY_TIMEOUT_MS` still work as temporary env overrides.
|
||||
- `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true` remains a loopback-only development escape hatch; do not persist HTTP relay URLs in config.
|
||||
|
||||
See [iOS App](/platforms/ios#relay-backed-push-for-official-builds) for the end-to-end flow and [Authentication and trust flow](/platforms/ios#authentication-and-trust-flow) for the relay security model.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Set up heartbeat (periodic check-ins)">
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
every: "30m",
|
||||
target: "last",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `every`: duration string (`30m`, `2h`). Set `0m` to disable.
|
||||
- `target`: `last` | `none` | `<channel-id>` (for example `discord`, `matrix`, `telegram`, or `whatsapp`)
|
||||
- `directPolicy`: `allow` (default) or `block` for DM-style heartbeat targets
|
||||
- See [Heartbeat](/gateway/heartbeat) for the full guide.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Configure cron jobs">
|
||||
```json5
|
||||
{
|
||||
cron: {
|
||||
enabled: true,
|
||||
maxConcurrentRuns: 2,
|
||||
sessionRetention: "24h",
|
||||
runLog: {
|
||||
maxBytes: "2mb",
|
||||
keepLines: 2000,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `sessionRetention`: prune completed isolated run sessions from `sessions.json` (default `24h`; set `false` to disable).
|
||||
- `runLog`: prune `cron/runs/<jobId>.jsonl` by size and retained lines.
|
||||
- See [Cron jobs](/automation/cron-jobs) for feature overview and CLI examples.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Set up webhooks (hooks)">
|
||||
Enable HTTP webhook endpoints on the Gateway:
|
||||
|
||||
```json5
|
||||
{
|
||||
hooks: {
|
||||
enabled: true,
|
||||
token: "shared-secret",
|
||||
path: "/hooks",
|
||||
defaultSessionKey: "hook:ingress",
|
||||
allowRequestSessionKey: false,
|
||||
allowedSessionKeyPrefixes: ["hook:"],
|
||||
mappings: [
|
||||
{
|
||||
match: { path: "gmail" },
|
||||
action: "agent",
|
||||
agentId: "main",
|
||||
deliver: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Security note:
|
||||
- Treat all hook/webhook payload content as untrusted input.
|
||||
- Use a dedicated `hooks.token`; do not reuse the shared Gateway token.
|
||||
- Hook auth is header-only (`Authorization: Bearer ...` or `x-openclaw-token`); query-string tokens are rejected.
|
||||
- `hooks.path` cannot be `/`; keep webhook ingress on a dedicated subpath such as `/hooks`.
|
||||
- Keep unsafe-content bypass flags disabled (`hooks.gmail.allowUnsafeExternalContent`, `hooks.mappings[].allowUnsafeExternalContent`) unless doing tightly scoped debugging.
|
||||
- If you enable `hooks.allowRequestSessionKey`, also set `hooks.allowedSessionKeyPrefixes` to bound caller-selected session keys.
|
||||
- For hook-driven agents, prefer strong modern model tiers and strict tool policy (for example messaging-only plus sandboxing where possible).
|
||||
|
||||
See [full reference](/gateway/configuration-reference#hooks) for all mapping options and Gmail integration.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Configure multi-agent routing">
|
||||
Run multiple isolated agents with separate workspaces and sessions:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "home", default: true, workspace: "~/.openclaw/workspace-home" },
|
||||
{ id: "work", workspace: "~/.openclaw/workspace-work" },
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
|
||||
{ agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
See [Multi-Agent](/concepts/multi-agent) and [full reference](/gateway/configuration-reference#multi-agent-routing) for binding rules and per-agent access profiles.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Split config into multiple files ($include)">
|
||||
Use `$include` to organize large configs:
|
||||
|
||||
```json5
|
||||
// ~/.openclaw/openclaw.json
|
||||
{
|
||||
gateway: { port: 18789 },
|
||||
agents: { $include: "./agents.json5" },
|
||||
broadcast: {
|
||||
$include: ["./clients/a.json5", "./clients/b.json5"],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- **Single file**: replaces the containing object
|
||||
- **Array of files**: deep-merged in order (later wins)
|
||||
- **Sibling keys**: merged after includes (override included values)
|
||||
- **Nested includes**: supported up to 10 levels deep
|
||||
- **Relative paths**: resolved relative to the including file
|
||||
- **Error handling**: clear errors for missing files, parse errors, and circular includes
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Config hot reload
|
||||
|
||||
The Gateway watches `~/.openclaw/openclaw.json` and applies changes automatically — no manual restart needed for most settings.
|
||||
|
||||
Direct file edits are treated as untrusted until they validate. The watcher waits
|
||||
for editor temp-write/rename churn to settle, reads the final file, and rejects
|
||||
invalid external edits by restoring the last-known-good config. OpenClaw-owned
|
||||
config writes use the same schema gate before writing; destructive clobbers such
|
||||
as dropping `gateway.mode` or shrinking the file by more than half are rejected
|
||||
and saved as `.rejected.*` for inspection.
|
||||
|
||||
If you see `Config auto-restored from last-known-good` or
|
||||
`config reload restored last-known-good config` in logs, inspect the matching
|
||||
`.clobbered.*` file next to `openclaw.json`, fix the rejected payload, then run
|
||||
`openclaw config validate`. See [Gateway troubleshooting](/gateway/troubleshooting#gateway-restored-last-known-good-config)
|
||||
for the recovery checklist.
|
||||
|
||||
### Reload modes
|
||||
|
||||
| Mode | Behavior |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------- |
|
||||
| **`hybrid`** (default) | Hot-applies safe changes instantly. Automatically restarts for critical ones. |
|
||||
| **`hot`** | Hot-applies safe changes only. Logs a warning when a restart is needed — you handle it. |
|
||||
| **`restart`** | Restarts the Gateway on any config change, safe or not. |
|
||||
| **`off`** | Disables file watching. Changes take effect on the next manual restart. |
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
reload: { mode: "hybrid", debounceMs: 300 },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### What hot-applies vs what needs a restart
|
||||
|
||||
Most fields hot-apply without downtime. In `hybrid` mode, restart-required changes are handled automatically.
|
||||
|
||||
| Category | Fields | Restart needed? |
|
||||
| ------------------- | -------------------------------------------------------------------- | --------------- |
|
||||
| Channels | `channels.*`, `web` (WhatsApp) — all built-in and extension channels | No |
|
||||
| Agent & models | `agent`, `agents`, `models`, `routing` | No |
|
||||
| Automation | `hooks`, `cron`, `agent.heartbeat` | No |
|
||||
| Sessions & messages | `session`, `messages` | No |
|
||||
| Tools & media | `tools`, `browser`, `skills`, `audio`, `talk` | No |
|
||||
| UI & misc | `ui`, `logging`, `identity`, `bindings` | No |
|
||||
| Gateway server | `gateway.*` (port, bind, auth, tailscale, TLS, HTTP) | **Yes** |
|
||||
| Infrastructure | `discovery`, `canvasHost`, `plugins` | **Yes** |
|
||||
|
||||
<Note>
|
||||
`gateway.reload` and `gateway.remote` are exceptions — changing them does **not** trigger a restart.
|
||||
</Note>
|
||||
|
||||
## Config RPC (programmatic updates)
|
||||
|
||||
<Note>
|
||||
Control-plane write RPCs (`config.apply`, `config.patch`, `update.run`) are rate-limited to **3 requests per 60 seconds** per `deviceId+clientIp`. When limited, the RPC returns `UNAVAILABLE` with `retryAfterMs`.
|
||||
</Note>
|
||||
|
||||
Safe/default flow:
|
||||
|
||||
- `config.schema.lookup`: inspect one path-scoped config subtree with a shallow
|
||||
schema node, matched hint metadata, and immediate child summaries
|
||||
- `config.get`: fetch the current snapshot + hash
|
||||
- `config.patch`: preferred partial update path
|
||||
- `config.apply`: full-config replacement only
|
||||
- `update.run`: explicit self-update + restart
|
||||
|
||||
When you are not replacing the entire config, prefer `config.schema.lookup`
|
||||
then `config.patch`.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="config.apply (full replace)">
|
||||
Validates + writes the full config and restarts the Gateway in one step.
|
||||
|
||||
<Warning>
|
||||
`config.apply` replaces the **entire config**. Use `config.patch` for partial updates, or `openclaw config set` for single keys.
|
||||
</Warning>
|
||||
|
||||
Params:
|
||||
|
||||
- `raw` (string) — JSON5 payload for the entire config
|
||||
- `baseHash` (optional) — config hash from `config.get` (required when config exists)
|
||||
- `sessionKey` (optional) — session key for the post-restart wake-up ping
|
||||
- `note` (optional) — note for the restart sentinel
|
||||
- `restartDelayMs` (optional) — delay before restart (default 2000)
|
||||
|
||||
Restart requests are coalesced while one is already pending/in-flight, and a 30-second cooldown applies between restart cycles.
|
||||
|
||||
```bash
|
||||
openclaw gateway call config.get --params '{}' # capture payload.hash
|
||||
openclaw gateway call config.apply --params '{
|
||||
"raw": "{ agents: { defaults: { workspace: \"~/.openclaw/workspace\" } } }",
|
||||
"baseHash": "<hash>",
|
||||
"sessionKey": "agent:main:whatsapp:direct:+15555550123"
|
||||
}'
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="config.patch (partial update)">
|
||||
Merges a partial update into the existing config (JSON merge patch semantics):
|
||||
|
||||
- Objects merge recursively
|
||||
- `null` deletes a key
|
||||
- Arrays replace
|
||||
|
||||
Params:
|
||||
|
||||
- `raw` (string) — JSON5 with just the keys to change
|
||||
- `baseHash` (required) — config hash from `config.get`
|
||||
- `sessionKey`, `note`, `restartDelayMs` — same as `config.apply`
|
||||
|
||||
Restart behavior matches `config.apply`: coalesced pending restarts plus a 30-second cooldown between restart cycles.
|
||||
|
||||
```bash
|
||||
openclaw gateway call config.patch --params '{
|
||||
"raw": "{ channels: { telegram: { groups: { \"*\": { requireMention: false } } } } }",
|
||||
"baseHash": "<hash>"
|
||||
}'
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Environment variables
|
||||
|
||||
OpenClaw reads env vars from the parent process plus:
|
||||
|
||||
- `.env` from the current working directory (if present)
|
||||
- `~/.openclaw/.env` (global fallback)
|
||||
|
||||
Neither file overrides existing env vars. You can also set inline env vars in config:
|
||||
|
||||
```json5
|
||||
{
|
||||
env: {
|
||||
OPENROUTER_API_KEY: "sk-or-...",
|
||||
vars: { GROQ_API_KEY: "gsk-..." },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Accordion title="Shell env import (optional)">
|
||||
If enabled and expected keys aren't set, OpenClaw runs your login shell and imports only the missing keys:
|
||||
|
||||
```json5
|
||||
{
|
||||
env: {
|
||||
shellEnv: { enabled: true, timeoutMs: 15000 },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Env var equivalent: `OPENCLAW_LOAD_SHELL_ENV=1`
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Env var substitution in config values">
|
||||
Reference env vars in any config string value with `${VAR_NAME}`:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: { auth: { token: "${OPENCLAW_GATEWAY_TOKEN}" } },
|
||||
models: { providers: { custom: { apiKey: "${CUSTOM_API_KEY}" } } },
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Only uppercase names matched: `[A-Z_][A-Z0-9_]*`
|
||||
- Missing/empty vars throw an error at load time
|
||||
- Escape with `$${VAR}` for literal output
|
||||
- Works inside `$include` files
|
||||
- Inline substitution: `"${BASE}/v1"` → `"https://api.example.com/v1"`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Secret refs (env, file, exec)">
|
||||
For fields that support SecretRef objects, you can use:
|
||||
|
||||
```json5
|
||||
{
|
||||
models: {
|
||||
providers: {
|
||||
openai: { apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" } },
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
entries: {
|
||||
"image-lab": {
|
||||
apiKey: {
|
||||
source: "file",
|
||||
provider: "filemain",
|
||||
id: "/skills/entries/image-lab/apiKey",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
googlechat: {
|
||||
serviceAccountRef: {
|
||||
source: "exec",
|
||||
provider: "vault",
|
||||
id: "channels/googlechat/serviceAccount",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
SecretRef details (including `secrets.providers` for `env`/`file`/`exec`) are in [Secrets Management](/gateway/secrets).
|
||||
Supported credential paths are listed in [SecretRef Credential Surface](/reference/secretref-credential-surface).
|
||||
</Accordion>
|
||||
|
||||
See [Environment](/help/environment) for full precedence and sources.
|
||||
|
||||
## Full reference
|
||||
|
||||
For the complete field-by-field reference, see **[Configuration Reference](/gateway/configuration-reference)**.
|
||||
|
||||
---
|
||||
|
||||
_Related: [Configuration Examples](/gateway/configuration-examples) · [Configuration Reference](/gateway/configuration-reference) · [Doctor](/gateway/doctor)_
|
||||
141
openclaw/docs/gateway/discovery.md
Normal file
141
openclaw/docs/gateway/discovery.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
---
|
||||
summary: "Node discovery and transports (Bonjour, Tailscale, SSH) for finding the gateway"
|
||||
read_when:
|
||||
- Implementing or changing Bonjour discovery/advertising
|
||||
- Adjusting remote connection modes (direct vs SSH)
|
||||
- Designing node discovery + pairing for remote nodes
|
||||
title: "Discovery and Transports"
|
||||
---
|
||||
|
||||
# Discovery & transports
|
||||
|
||||
OpenClaw has two distinct problems that look similar on the surface:
|
||||
|
||||
1. **Operator remote control**: the macOS menu bar app controlling a gateway running elsewhere.
|
||||
2. **Node pairing**: iOS/Android (and future nodes) finding a gateway and pairing securely.
|
||||
|
||||
The design goal is to keep all network discovery/advertising in the **Node Gateway** (`openclaw gateway`) and keep clients (mac app, iOS) as consumers.
|
||||
|
||||
## Terms
|
||||
|
||||
- **Gateway**: a single long-running gateway process that owns state (sessions, pairing, node registry) and runs channels. Most setups use one per host; isolated multi-gateway setups are possible.
|
||||
- **Gateway WS (control plane)**: the WebSocket endpoint on `127.0.0.1:18789` by default; can be bound to LAN/tailnet via `gateway.bind`.
|
||||
- **Direct WS transport**: a LAN/tailnet-facing Gateway WS endpoint (no SSH).
|
||||
- **SSH transport (fallback)**: remote control by forwarding `127.0.0.1:18789` over SSH.
|
||||
- **Legacy TCP bridge (removed)**: older node transport (see
|
||||
[Bridge protocol](/gateway/bridge-protocol)); no longer advertised for
|
||||
discovery and no longer part of current builds.
|
||||
|
||||
Protocol details:
|
||||
|
||||
- [Gateway protocol](/gateway/protocol)
|
||||
- [Bridge protocol (legacy)](/gateway/bridge-protocol)
|
||||
|
||||
## Why we keep both "direct" and SSH
|
||||
|
||||
- **Direct WS** is the best UX on the same network and within a tailnet:
|
||||
- auto-discovery on LAN via Bonjour
|
||||
- pairing tokens + ACLs owned by the gateway
|
||||
- no shell access required; protocol surface can stay tight and auditable
|
||||
- **SSH** remains the universal fallback:
|
||||
- works anywhere you have SSH access (even across unrelated networks)
|
||||
- survives multicast/mDNS issues
|
||||
- requires no new inbound ports besides SSH
|
||||
|
||||
## Discovery inputs (how clients learn where the gateway is)
|
||||
|
||||
### 1) Bonjour / DNS-SD discovery
|
||||
|
||||
Multicast Bonjour is best-effort and does not cross networks. OpenClaw can also browse the
|
||||
same gateway beacon via a configured wide-area DNS-SD domain, so discovery can cover:
|
||||
|
||||
- `local.` on the same LAN
|
||||
- a configured unicast DNS-SD domain for cross-network discovery
|
||||
|
||||
Target direction:
|
||||
|
||||
- The **gateway** advertises its WS endpoint via Bonjour.
|
||||
- Clients browse and show a “pick a gateway” list, then store the chosen endpoint.
|
||||
|
||||
Troubleshooting and beacon details: [Bonjour](/gateway/bonjour).
|
||||
|
||||
#### Service beacon details
|
||||
|
||||
- Service types:
|
||||
- `_openclaw-gw._tcp` (gateway transport beacon)
|
||||
- TXT keys (non-secret):
|
||||
- `role=gateway`
|
||||
- `transport=gateway`
|
||||
- `displayName=<friendly name>` (operator-configured display name)
|
||||
- `lanHost=<hostname>.local`
|
||||
- `gatewayPort=18789` (Gateway WS + HTTP)
|
||||
- `gatewayTls=1` (only when TLS is enabled)
|
||||
- `gatewayTlsSha256=<sha256>` (only when TLS is enabled and fingerprint is available)
|
||||
- `canvasPort=<port>` (canvas host port; currently the same as `gatewayPort` when the canvas host is enabled)
|
||||
- `tailnetDns=<magicdns>` (optional hint; auto-detected when Tailscale is available)
|
||||
- `sshPort=<port>` (mDNS full mode only; wide-area DNS-SD may omit it, in which case SSH defaults stay at `22`)
|
||||
- `cliPath=<path>` (mDNS full mode only; wide-area DNS-SD still writes it as a remote-install hint)
|
||||
|
||||
Security notes:
|
||||
|
||||
- Bonjour/mDNS TXT records are **unauthenticated**. Clients must treat TXT values as UX hints only.
|
||||
- Routing (host/port) should prefer the **resolved service endpoint** (SRV + A/AAAA) over TXT-provided `lanHost`, `tailnetDns`, or `gatewayPort`.
|
||||
- TLS pinning must never allow an advertised `gatewayTlsSha256` to override a previously stored pin.
|
||||
- iOS/Android nodes should require an explicit “trust this fingerprint” confirmation before storing a first-time pin (out-of-band verification) whenever the chosen route is secure/TLS-based.
|
||||
|
||||
Disable/override:
|
||||
|
||||
- `OPENCLAW_DISABLE_BONJOUR=1` disables advertising.
|
||||
- `gateway.bind` in `~/.openclaw/openclaw.json` controls the Gateway bind mode.
|
||||
- `OPENCLAW_SSH_PORT` overrides the SSH port advertised when `sshPort` is emitted.
|
||||
- `OPENCLAW_TAILNET_DNS` publishes a `tailnetDns` hint (MagicDNS).
|
||||
- `OPENCLAW_CLI_PATH` overrides the advertised CLI path.
|
||||
|
||||
### 2) Tailnet (cross-network)
|
||||
|
||||
For London/Vienna style setups, Bonjour won’t help. The recommended “direct” target is:
|
||||
|
||||
- Tailscale MagicDNS name (preferred) or a stable tailnet IP.
|
||||
|
||||
If the gateway can detect it is running under Tailscale, it publishes `tailnetDns` as an optional hint for clients (including wide-area beacons).
|
||||
|
||||
The macOS app now prefers MagicDNS names over raw Tailscale IPs for gateway discovery. This improves reliability when tailnet IPs change (for example after node restarts or CGNAT reassignment), because MagicDNS names resolve to the current IP automatically.
|
||||
|
||||
For mobile node pairing, discovery hints do not relax transport security on tailnet/public routes:
|
||||
|
||||
- iOS/Android still require a secure first-time tailnet/public connect path (`wss://` or Tailscale Serve/Funnel).
|
||||
- A discovered raw tailnet IP is a routing hint, not permission to use plaintext remote `ws://`.
|
||||
- Private LAN direct-connect `ws://` remains supported.
|
||||
- If you want the simplest Tailscale path for mobile nodes, use Tailscale Serve so discovery and the setup code both resolve to the same secure MagicDNS endpoint.
|
||||
|
||||
### 3) Manual / SSH target
|
||||
|
||||
When there is no direct route (or direct is disabled), clients can always connect via SSH by forwarding the loopback gateway port.
|
||||
|
||||
See [Remote access](/gateway/remote).
|
||||
|
||||
## Transport selection (client policy)
|
||||
|
||||
Recommended client behavior:
|
||||
|
||||
1. If a paired direct endpoint is configured and reachable, use it.
|
||||
2. Else, if discovery finds a gateway on `local.` or the configured wide-area domain, offer a one-tap “Use this gateway” choice and save it as the direct endpoint.
|
||||
3. Else, if a tailnet DNS/IP is configured, try direct.
|
||||
For mobile nodes on tailnet/public routes, direct means a secure endpoint, not plaintext remote `ws://`.
|
||||
4. Else, fall back to SSH.
|
||||
|
||||
## Pairing + auth (direct transport)
|
||||
|
||||
The gateway is the source of truth for node/client admission.
|
||||
|
||||
- Pairing requests are created/approved/rejected in the gateway (see [Gateway pairing](/gateway/pairing)).
|
||||
- The gateway enforces:
|
||||
- auth (token / keypair)
|
||||
- scopes/ACLs (the gateway is not a raw proxy to every method)
|
||||
- rate limits
|
||||
|
||||
## Responsibilities by component
|
||||
|
||||
- **Gateway**: advertises discovery beacons, owns pairing decisions, and hosts the WS endpoint.
|
||||
- **macOS app**: helps you pick a gateway, shows pairing prompts, and uses SSH only as a fallback.
|
||||
- **iOS/Android nodes**: browse Bonjour as a convenience and connect to the paired Gateway WS.
|
||||
573
openclaw/docs/gateway/doctor.md
Normal file
573
openclaw/docs/gateway/doctor.md
Normal file
|
|
@ -0,0 +1,573 @@
|
|||
---
|
||||
summary: "Doctor command: health checks, config migrations, and repair steps"
|
||||
read_when:
|
||||
- Adding or modifying doctor migrations
|
||||
- Introducing breaking config changes
|
||||
title: "Doctor"
|
||||
---
|
||||
|
||||
# Doctor
|
||||
|
||||
`openclaw doctor` is the repair + migration tool for OpenClaw. It fixes stale
|
||||
config/state, checks health, and provides actionable repair steps.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
### Headless / automation
|
||||
|
||||
```bash
|
||||
openclaw doctor --yes
|
||||
```
|
||||
|
||||
Accept defaults without prompting (including restart/service/sandbox repair steps when applicable).
|
||||
|
||||
```bash
|
||||
openclaw doctor --repair
|
||||
```
|
||||
|
||||
Apply recommended repairs without prompting (repairs + restarts where safe).
|
||||
|
||||
```bash
|
||||
openclaw doctor --repair --force
|
||||
```
|
||||
|
||||
Apply aggressive repairs too (overwrites custom supervisor configs).
|
||||
|
||||
```bash
|
||||
openclaw doctor --non-interactive
|
||||
```
|
||||
|
||||
Run without prompts and only apply safe migrations (config normalization + on-disk state moves). Skips restart/service/sandbox actions that require human confirmation.
|
||||
Legacy state migrations run automatically when detected.
|
||||
|
||||
```bash
|
||||
openclaw doctor --deep
|
||||
```
|
||||
|
||||
Scan system services for extra gateway installs (launchd/systemd/schtasks).
|
||||
|
||||
If you want to review changes before writing, open the config file first:
|
||||
|
||||
```bash
|
||||
cat ~/.openclaw/openclaw.json
|
||||
```
|
||||
|
||||
## What it does (summary)
|
||||
|
||||
- Optional pre-flight update for git installs (interactive only).
|
||||
- UI protocol freshness check (rebuilds Control UI when the protocol schema is newer).
|
||||
- Health check + restart prompt.
|
||||
- Skills status summary (eligible/missing/blocked) and plugin status.
|
||||
- Config normalization for legacy values.
|
||||
- Talk config migration from legacy flat `talk.*` fields into `talk.provider` + `talk.providers.<provider>`.
|
||||
- Browser migration checks for legacy Chrome extension configs and Chrome MCP readiness.
|
||||
- OpenCode provider override warnings (`models.providers.opencode` / `models.providers.opencode-go`).
|
||||
- Codex OAuth shadowing warnings (`models.providers.openai-codex`).
|
||||
- OAuth TLS prerequisites check for OpenAI Codex OAuth profiles.
|
||||
- Legacy on-disk state migration (sessions/agent dir/WhatsApp auth).
|
||||
- Legacy plugin manifest contract key migration (`speechProviders`, `realtimeTranscriptionProviders`, `realtimeVoiceProviders`, `mediaUnderstandingProviders`, `imageGenerationProviders`, `videoGenerationProviders`, `webFetchProviders`, `webSearchProviders` → `contracts`).
|
||||
- Legacy cron store migration (`jobId`, `schedule.cron`, top-level delivery/payload fields, payload `provider`, simple `notify: true` webhook fallback jobs).
|
||||
- Session lock file inspection and stale lock cleanup.
|
||||
- State integrity and permissions checks (sessions, transcripts, state dir).
|
||||
- Config file permission checks (chmod 600) when running locally.
|
||||
- Model auth health: checks OAuth expiry, can refresh expiring tokens, and reports auth-profile cooldown/disabled states.
|
||||
- Extra workspace dir detection (`~/openclaw`).
|
||||
- Sandbox image repair when sandboxing is enabled.
|
||||
- Legacy service migration and extra gateway detection.
|
||||
- Matrix channel legacy state migration (in `--fix` / `--repair` mode).
|
||||
- Gateway runtime checks (service installed but not running; cached launchd label).
|
||||
- Channel status warnings (probed from the running gateway).
|
||||
- Supervisor config audit (launchd/systemd/schtasks) with optional repair.
|
||||
- Gateway runtime best-practice checks (Node vs Bun, version-manager paths).
|
||||
- Gateway port collision diagnostics (default `18789`).
|
||||
- Security warnings for open DM policies.
|
||||
- Gateway auth checks for local token mode (offers token generation when no token source exists; does not overwrite token SecretRef configs).
|
||||
- Device pairing trouble detection (pending first-time pair requests, pending role/scope upgrades, stale local device-token cache drift, and paired-record auth drift).
|
||||
- systemd linger check on Linux.
|
||||
- Workspace bootstrap file size check (truncation/near-limit warnings for context files).
|
||||
- Shell completion status check and auto-install/upgrade.
|
||||
- Memory search embedding provider readiness check (local model, remote API key, or QMD binary).
|
||||
- Source install checks (pnpm workspace mismatch, missing UI assets, missing tsx binary).
|
||||
- Writes updated config + wizard metadata.
|
||||
|
||||
## Dreams UI backfill and reset
|
||||
|
||||
The Control UI Dreams scene includes **Backfill**, **Reset**, and **Clear Grounded**
|
||||
actions for the grounded dreaming workflow. These actions use gateway
|
||||
doctor-style RPC methods, but they are **not** part of `openclaw doctor` CLI
|
||||
repair/migration.
|
||||
|
||||
What they do:
|
||||
|
||||
- **Backfill** scans historical `memory/YYYY-MM-DD.md` files in the active
|
||||
workspace, runs the grounded REM diary pass, and writes reversible backfill
|
||||
entries into `DREAMS.md`.
|
||||
- **Reset** removes only those marked backfill diary entries from `DREAMS.md`.
|
||||
- **Clear Grounded** removes only staged grounded-only short-term entries that
|
||||
came from historical replay and have not accumulated live recall or daily
|
||||
support yet.
|
||||
|
||||
What they do **not** do by themselves:
|
||||
|
||||
- they do not edit `MEMORY.md`
|
||||
- they do not run full doctor migrations
|
||||
- they do not automatically stage grounded candidates into the live short-term
|
||||
promotion store unless you explicitly run the staged CLI path first
|
||||
|
||||
If you want grounded historical replay to influence the normal deep promotion
|
||||
lane, use the CLI flow instead:
|
||||
|
||||
```bash
|
||||
openclaw memory rem-backfill --path ./memory --stage-short-term
|
||||
```
|
||||
|
||||
That stages grounded durable candidates into the short-term dreaming store while
|
||||
keeping `DREAMS.md` as the review surface.
|
||||
|
||||
## Detailed behavior and rationale
|
||||
|
||||
### 0) Optional update (git installs)
|
||||
|
||||
If this is a git checkout and doctor is running interactively, it offers to
|
||||
update (fetch/rebase/build) before running doctor.
|
||||
|
||||
### 1) Config normalization
|
||||
|
||||
If the config contains legacy value shapes (for example `messages.ackReaction`
|
||||
without a channel-specific override), doctor normalizes them into the current
|
||||
schema.
|
||||
|
||||
That includes legacy Talk flat fields. Current public Talk config is
|
||||
`talk.provider` + `talk.providers.<provider>`. Doctor rewrites old
|
||||
`talk.voiceId` / `talk.voiceAliases` / `talk.modelId` / `talk.outputFormat` /
|
||||
`talk.apiKey` shapes into the provider map.
|
||||
|
||||
### 2) Legacy config key migrations
|
||||
|
||||
When the config contains deprecated keys, other commands refuse to run and ask
|
||||
you to run `openclaw doctor`.
|
||||
|
||||
Doctor will:
|
||||
|
||||
- Explain which legacy keys were found.
|
||||
- Show the migration it applied.
|
||||
- Rewrite `~/.openclaw/openclaw.json` with the updated schema.
|
||||
|
||||
The Gateway also auto-runs doctor migrations on startup when it detects a
|
||||
legacy config format, so stale configs are repaired without manual intervention.
|
||||
Cron job store migrations are handled by `openclaw doctor --fix`.
|
||||
|
||||
Current migrations:
|
||||
|
||||
- `routing.allowFrom` → `channels.whatsapp.allowFrom`
|
||||
- `routing.groupChat.requireMention` → `channels.whatsapp/telegram/imessage.groups."*".requireMention`
|
||||
- `routing.groupChat.historyLimit` → `messages.groupChat.historyLimit`
|
||||
- `routing.groupChat.mentionPatterns` → `messages.groupChat.mentionPatterns`
|
||||
- `routing.queue` → `messages.queue`
|
||||
- `routing.bindings` → top-level `bindings`
|
||||
- `routing.agents`/`routing.defaultAgentId` → `agents.list` + `agents.list[].default`
|
||||
- legacy `talk.voiceId`/`talk.voiceAliases`/`talk.modelId`/`talk.outputFormat`/`talk.apiKey` → `talk.provider` + `talk.providers.<provider>`
|
||||
- `routing.agentToAgent` → `tools.agentToAgent`
|
||||
- `routing.transcribeAudio` → `tools.media.audio.models`
|
||||
- `messages.tts.<provider>` (`openai`/`elevenlabs`/`microsoft`/`edge`) → `messages.tts.providers.<provider>`
|
||||
- `channels.discord.voice.tts.<provider>` (`openai`/`elevenlabs`/`microsoft`/`edge`) → `channels.discord.voice.tts.providers.<provider>`
|
||||
- `channels.discord.accounts.<id>.voice.tts.<provider>` (`openai`/`elevenlabs`/`microsoft`/`edge`) → `channels.discord.accounts.<id>.voice.tts.providers.<provider>`
|
||||
- `plugins.entries.voice-call.config.tts.<provider>` (`openai`/`elevenlabs`/`microsoft`/`edge`) → `plugins.entries.voice-call.config.tts.providers.<provider>`
|
||||
- `plugins.entries.voice-call.config.provider: "log"` → `"mock"`
|
||||
- `plugins.entries.voice-call.config.twilio.from` → `plugins.entries.voice-call.config.fromNumber`
|
||||
- `plugins.entries.voice-call.config.streaming.sttProvider` → `plugins.entries.voice-call.config.streaming.provider`
|
||||
- `plugins.entries.voice-call.config.streaming.openaiApiKey|sttModel|silenceDurationMs|vadThreshold`
|
||||
→ `plugins.entries.voice-call.config.streaming.providers.openai.*`
|
||||
- `bindings[].match.accountID` → `bindings[].match.accountId`
|
||||
- For channels with named `accounts` but lingering single-account top-level channel values, move those account-scoped values into the promoted account chosen for that channel (`accounts.default` for most channels; Matrix can preserve an existing matching named/default target)
|
||||
- `identity` → `agents.list[].identity`
|
||||
- `agent.*` → `agents.defaults` + `tools.*` (tools/elevated/exec/sandbox/subagents)
|
||||
- `agent.model`/`allowedModels`/`modelAliases`/`modelFallbacks`/`imageModelFallbacks`
|
||||
→ `agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks`
|
||||
- `browser.ssrfPolicy.allowPrivateNetwork` → `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork`
|
||||
- `browser.profiles.*.driver: "extension"` → `"existing-session"`
|
||||
- remove `browser.relayBindHost` (legacy extension relay setting)
|
||||
|
||||
Doctor warnings also include account-default guidance for multi-account channels:
|
||||
|
||||
- If two or more `channels.<channel>.accounts` entries are configured without `channels.<channel>.defaultAccount` or `accounts.default`, doctor warns that fallback routing can pick an unexpected account.
|
||||
- If `channels.<channel>.defaultAccount` is set to an unknown account ID, doctor warns and lists configured account IDs.
|
||||
|
||||
### 2b) OpenCode provider overrides
|
||||
|
||||
If you’ve added `models.providers.opencode`, `opencode-zen`, or `opencode-go`
|
||||
manually, it overrides the built-in OpenCode catalog from `@mariozechner/pi-ai`.
|
||||
That can force models onto the wrong API or zero out costs. Doctor warns so you
|
||||
can remove the override and restore per-model API routing + costs.
|
||||
|
||||
### 2c) Browser migration and Chrome MCP readiness
|
||||
|
||||
If your browser config still points at the removed Chrome extension path, doctor
|
||||
normalizes it to the current host-local Chrome MCP attach model:
|
||||
|
||||
- `browser.profiles.*.driver: "extension"` becomes `"existing-session"`
|
||||
- `browser.relayBindHost` is removed
|
||||
|
||||
Doctor also audits the host-local Chrome MCP path when you use `defaultProfile:
|
||||
"user"` or a configured `existing-session` profile:
|
||||
|
||||
- checks whether Google Chrome is installed on the same host for default
|
||||
auto-connect profiles
|
||||
- checks the detected Chrome version and warns when it is below Chrome 144
|
||||
- reminds you to enable remote debugging in the browser inspect page (for
|
||||
example `chrome://inspect/#remote-debugging`, `brave://inspect/#remote-debugging`,
|
||||
or `edge://inspect/#remote-debugging`)
|
||||
|
||||
Doctor cannot enable the Chrome-side setting for you. Host-local Chrome MCP
|
||||
still requires:
|
||||
|
||||
- a Chromium-based browser 144+ on the gateway/node host
|
||||
- the browser running locally
|
||||
- remote debugging enabled in that browser
|
||||
- approving the first attach consent prompt in the browser
|
||||
|
||||
Readiness here is only about local attach prerequisites. Existing-session keeps
|
||||
the current Chrome MCP route limits; advanced routes like `responsebody`, PDF
|
||||
export, download interception, and batch actions still require a managed
|
||||
browser or raw CDP profile.
|
||||
|
||||
This check does **not** apply to Docker, sandbox, remote-browser, or other
|
||||
headless flows. Those continue to use raw CDP.
|
||||
|
||||
### 2d) OAuth TLS prerequisites
|
||||
|
||||
When an OpenAI Codex OAuth profile is configured, doctor probes the OpenAI
|
||||
authorization endpoint to verify that the local Node/OpenSSL TLS stack can
|
||||
validate the certificate chain. If the probe fails with a certificate error (for
|
||||
example `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`, expired cert, or self-signed cert),
|
||||
doctor prints platform-specific fix guidance. On macOS with a Homebrew Node, the
|
||||
fix is usually `brew postinstall ca-certificates`. With `--deep`, the probe runs
|
||||
even if the gateway is healthy.
|
||||
|
||||
### 2c) Codex OAuth provider overrides
|
||||
|
||||
If you previously added legacy OpenAI transport settings under
|
||||
`models.providers.openai-codex`, they can shadow the built-in Codex OAuth
|
||||
provider path that newer releases use automatically. Doctor warns when it sees
|
||||
those old transport settings alongside Codex OAuth so you can remove or rewrite
|
||||
the stale transport override and get the built-in routing/fallback behavior
|
||||
back. Custom proxies and header-only overrides are still supported and do not
|
||||
trigger this warning.
|
||||
|
||||
### 3) Legacy state migrations (disk layout)
|
||||
|
||||
Doctor can migrate older on-disk layouts into the current structure:
|
||||
|
||||
- Sessions store + transcripts:
|
||||
- from `~/.openclaw/sessions/` to `~/.openclaw/agents/<agentId>/sessions/`
|
||||
- Agent dir:
|
||||
- from `~/.openclaw/agent/` to `~/.openclaw/agents/<agentId>/agent/`
|
||||
- WhatsApp auth state (Baileys):
|
||||
- from legacy `~/.openclaw/credentials/*.json` (except `oauth.json`)
|
||||
- to `~/.openclaw/credentials/whatsapp/<accountId>/...` (default account id: `default`)
|
||||
|
||||
These migrations are best-effort and idempotent; doctor will emit warnings when
|
||||
it leaves any legacy folders behind as backups. The Gateway/CLI also auto-migrates
|
||||
the legacy sessions + agent dir on startup so history/auth/models land in the
|
||||
per-agent path without a manual doctor run. WhatsApp auth is intentionally only
|
||||
migrated via `openclaw doctor`. Talk provider/provider-map normalization now
|
||||
compares by structural equality, so key-order-only diffs no longer trigger
|
||||
repeat no-op `doctor --fix` changes.
|
||||
|
||||
### 3a) Legacy plugin manifest migrations
|
||||
|
||||
Doctor scans all installed plugin manifests for deprecated top-level capability
|
||||
keys (`speechProviders`, `realtimeTranscriptionProviders`,
|
||||
`realtimeVoiceProviders`, `mediaUnderstandingProviders`,
|
||||
`imageGenerationProviders`, `videoGenerationProviders`, `webFetchProviders`,
|
||||
`webSearchProviders`). When found, it offers to move them into the `contracts`
|
||||
object and rewrite the manifest file in-place. This migration is idempotent;
|
||||
if the `contracts` key already has the same values, the legacy key is removed
|
||||
without duplicating the data.
|
||||
|
||||
### 3b) Legacy cron store migrations
|
||||
|
||||
Doctor also checks the cron job store (`~/.openclaw/cron/jobs.json` by default,
|
||||
or `cron.store` when overridden) for old job shapes that the scheduler still
|
||||
accepts for compatibility.
|
||||
|
||||
Current cron cleanups include:
|
||||
|
||||
- `jobId` → `id`
|
||||
- `schedule.cron` → `schedule.expr`
|
||||
- top-level payload fields (`message`, `model`, `thinking`, ...) → `payload`
|
||||
- top-level delivery fields (`deliver`, `channel`, `to`, `provider`, ...) → `delivery`
|
||||
- payload `provider` delivery aliases → explicit `delivery.channel`
|
||||
- simple legacy `notify: true` webhook fallback jobs → explicit `delivery.mode="webhook"` with `delivery.to=cron.webhook`
|
||||
|
||||
Doctor only auto-migrates `notify: true` jobs when it can do so without
|
||||
changing behavior. If a job combines legacy notify fallback with an existing
|
||||
non-webhook delivery mode, doctor warns and leaves that job for manual review.
|
||||
|
||||
### 3c) Session lock cleanup
|
||||
|
||||
Doctor scans every agent session directory for stale write-lock files — files left
|
||||
behind when a session exited abnormally. For each lock file found it reports:
|
||||
the path, PID, whether the PID is still alive, lock age, and whether it is
|
||||
considered stale (dead PID or older than 30 minutes). In `--fix` / `--repair`
|
||||
mode it removes stale lock files automatically; otherwise it prints a note and
|
||||
instructs you to rerun with `--fix`.
|
||||
|
||||
### 4) State integrity checks (session persistence, routing, and safety)
|
||||
|
||||
The state directory is the operational brainstem. If it vanishes, you lose
|
||||
sessions, credentials, logs, and config (unless you have backups elsewhere).
|
||||
|
||||
Doctor checks:
|
||||
|
||||
- **State dir missing**: warns about catastrophic state loss, prompts to recreate
|
||||
the directory, and reminds you that it cannot recover missing data.
|
||||
- **State dir permissions**: verifies writability; offers to repair permissions
|
||||
(and emits a `chown` hint when owner/group mismatch is detected).
|
||||
- **macOS cloud-synced state dir**: warns when state resolves under iCloud Drive
|
||||
(`~/Library/Mobile Documents/com~apple~CloudDocs/...`) or
|
||||
`~/Library/CloudStorage/...` because sync-backed paths can cause slower I/O
|
||||
and lock/sync races.
|
||||
- **Linux SD or eMMC state dir**: warns when state resolves to an `mmcblk*`
|
||||
mount source, because SD or eMMC-backed random I/O can be slower and wear
|
||||
faster under session and credential writes.
|
||||
- **Session dirs missing**: `sessions/` and the session store directory are
|
||||
required to persist history and avoid `ENOENT` crashes.
|
||||
- **Transcript mismatch**: warns when recent session entries have missing
|
||||
transcript files.
|
||||
- **Main session “1-line JSONL”**: flags when the main transcript has only one
|
||||
line (history is not accumulating).
|
||||
- **Multiple state dirs**: warns when multiple `~/.openclaw` folders exist across
|
||||
home directories or when `OPENCLAW_STATE_DIR` points elsewhere (history can
|
||||
split between installs).
|
||||
- **Remote mode reminder**: if `gateway.mode=remote`, doctor reminds you to run
|
||||
it on the remote host (the state lives there).
|
||||
- **Config file permissions**: warns if `~/.openclaw/openclaw.json` is
|
||||
group/world readable and offers to tighten to `600`.
|
||||
|
||||
### 5) Model auth health (OAuth expiry)
|
||||
|
||||
Doctor inspects OAuth profiles in the auth store, warns when tokens are
|
||||
expiring/expired, and can refresh them when safe. If the Anthropic
|
||||
OAuth/token profile is stale, it suggests an Anthropic API key or the
|
||||
Anthropic setup-token path.
|
||||
Refresh prompts only appear when running interactively (TTY); `--non-interactive`
|
||||
skips refresh attempts.
|
||||
|
||||
When an OAuth refresh fails permanently (for example `refresh_token_reused`,
|
||||
`invalid_grant`, or a provider telling you to sign in again), doctor reports
|
||||
that re-auth is required and prints the exact `openclaw models auth login --provider ...`
|
||||
command to run.
|
||||
|
||||
Doctor also reports auth profiles that are temporarily unusable due to:
|
||||
|
||||
- short cooldowns (rate limits/timeouts/auth failures)
|
||||
- longer disables (billing/credit failures)
|
||||
|
||||
### 6) Hooks model validation
|
||||
|
||||
If `hooks.gmail.model` is set, doctor validates the model reference against the
|
||||
catalog and allowlist and warns when it won’t resolve or is disallowed.
|
||||
|
||||
### 7) Sandbox image repair
|
||||
|
||||
When sandboxing is enabled, doctor checks Docker images and offers to build or
|
||||
switch to legacy names if the current image is missing.
|
||||
|
||||
### 7b) Bundled plugin runtime deps
|
||||
|
||||
Doctor verifies that bundled plugin runtime dependencies (for example the
|
||||
Discord plugin runtime packages) are present in the OpenClaw install root.
|
||||
If any are missing, doctor reports the packages and installs them in
|
||||
`openclaw doctor --fix` / `openclaw doctor --repair` mode.
|
||||
|
||||
### 8) Gateway service migrations and cleanup hints
|
||||
|
||||
Doctor detects legacy gateway services (launchd/systemd/schtasks) and
|
||||
offers to remove them and install the OpenClaw service using the current gateway
|
||||
port. It can also scan for extra gateway-like services and print cleanup hints.
|
||||
Profile-named OpenClaw gateway services are considered first-class and are not
|
||||
flagged as "extra."
|
||||
|
||||
### 8b) Startup Matrix migration
|
||||
|
||||
When a Matrix channel account has a pending or actionable legacy state migration,
|
||||
doctor (in `--fix` / `--repair` mode) creates a pre-migration snapshot and then
|
||||
runs the best-effort migration steps: legacy Matrix state migration and legacy
|
||||
encrypted-state preparation. Both steps are non-fatal; errors are logged and
|
||||
startup continues. In read-only mode (`openclaw doctor` without `--fix`) this check
|
||||
is skipped entirely.
|
||||
|
||||
### 8c) Device pairing and auth drift
|
||||
|
||||
Doctor now inspects device-pairing state as part of the normal health pass.
|
||||
|
||||
What it reports:
|
||||
|
||||
- pending first-time pairing requests
|
||||
- pending role upgrades for already paired devices
|
||||
- pending scope upgrades for already paired devices
|
||||
- public-key mismatch repairs where the device id still matches but the device
|
||||
identity no longer matches the approved record
|
||||
- paired records missing an active token for an approved role
|
||||
- paired tokens whose scopes drift outside the approved pairing baseline
|
||||
- local cached device-token entries for the current machine that predate a
|
||||
gateway-side token rotation or carry stale scope metadata
|
||||
|
||||
Doctor does not auto-approve pair requests or auto-rotate device tokens. It
|
||||
prints the exact next steps instead:
|
||||
|
||||
- inspect pending requests with `openclaw devices list`
|
||||
- approve the exact request with `openclaw devices approve <requestId>`
|
||||
- rotate a fresh token with `openclaw devices rotate --device <deviceId> --role <role>`
|
||||
- remove and re-approve a stale record with `openclaw devices remove <deviceId>`
|
||||
|
||||
This closes the common "already paired but still getting pairing required"
|
||||
hole: doctor now distinguishes first-time pairing from pending role/scope
|
||||
upgrades and from stale token/device-identity drift.
|
||||
|
||||
### 9) Security warnings
|
||||
|
||||
Doctor emits warnings when a provider is open to DMs without an allowlist, or
|
||||
when a policy is configured in a dangerous way.
|
||||
|
||||
### 10) systemd linger (Linux)
|
||||
|
||||
If running as a systemd user service, doctor ensures lingering is enabled so the
|
||||
gateway stays alive after logout.
|
||||
|
||||
### 11) Workspace status (skills, plugins, and legacy dirs)
|
||||
|
||||
Doctor prints a summary of the workspace state for the default agent:
|
||||
|
||||
- **Skills status**: counts eligible, missing-requirements, and allowlist-blocked skills.
|
||||
- **Legacy workspace dirs**: warns when `~/openclaw` or other legacy workspace directories
|
||||
exist alongside the current workspace.
|
||||
- **Plugin status**: counts loaded/disabled/errored plugins; lists plugin IDs for any
|
||||
errors; reports bundle plugin capabilities.
|
||||
- **Plugin compatibility warnings**: flags plugins that have compatibility issues with
|
||||
the current runtime.
|
||||
- **Plugin diagnostics**: surfaces any load-time warnings or errors emitted by the
|
||||
plugin registry.
|
||||
|
||||
### 11b) Bootstrap file size
|
||||
|
||||
Doctor checks whether workspace bootstrap files (for example `AGENTS.md`,
|
||||
`CLAUDE.md`, or other injected context files) are near or over the configured
|
||||
character budget. It reports per-file raw vs. injected character counts, truncation
|
||||
percentage, truncation cause (`max/file` or `max/total`), and total injected
|
||||
characters as a fraction of the total budget. When files are truncated or near
|
||||
the limit, doctor prints tips for tuning `agents.defaults.bootstrapMaxChars`
|
||||
and `agents.defaults.bootstrapTotalMaxChars`.
|
||||
|
||||
### 11c) Shell completion
|
||||
|
||||
Doctor checks whether tab completion is installed for the current shell
|
||||
(zsh, bash, fish, or PowerShell):
|
||||
|
||||
- If the shell profile uses a slow dynamic completion pattern
|
||||
(`source <(openclaw completion ...)`), doctor upgrades it to the faster
|
||||
cached file variant.
|
||||
- If completion is configured in the profile but the cache file is missing,
|
||||
doctor regenerates the cache automatically.
|
||||
- If no completion is configured at all, doctor prompts to install it
|
||||
(interactive mode only; skipped with `--non-interactive`).
|
||||
|
||||
Run `openclaw completion --write-state` to regenerate the cache manually.
|
||||
|
||||
### 12) Gateway auth checks (local token)
|
||||
|
||||
Doctor checks local gateway token auth readiness.
|
||||
|
||||
- If token mode needs a token and no token source exists, doctor offers to generate one.
|
||||
- If `gateway.auth.token` is SecretRef-managed but unavailable, doctor warns and does not overwrite it with plaintext.
|
||||
- `openclaw doctor --generate-gateway-token` forces generation only when no token SecretRef is configured.
|
||||
|
||||
### 12b) Read-only SecretRef-aware repairs
|
||||
|
||||
Some repair flows need to inspect configured credentials without weakening runtime fail-fast behavior.
|
||||
|
||||
- `openclaw doctor --fix` now uses the same read-only SecretRef summary model as status-family commands for targeted config repairs.
|
||||
- Example: Telegram `allowFrom` / `groupAllowFrom` `@username` repair tries to use configured bot credentials when available.
|
||||
- If the Telegram bot token is configured via SecretRef but unavailable in the current command path, doctor reports that the credential is configured-but-unavailable and skips auto-resolution instead of crashing or misreporting the token as missing.
|
||||
|
||||
### 13) Gateway health check + restart
|
||||
|
||||
Doctor runs a health check and offers to restart the gateway when it looks
|
||||
unhealthy.
|
||||
|
||||
### 13b) Memory search readiness
|
||||
|
||||
Doctor checks whether the configured memory search embedding provider is ready
|
||||
for the default agent. The behavior depends on the configured backend and provider:
|
||||
|
||||
- **QMD backend**: probes whether the `qmd` binary is available and startable.
|
||||
If not, prints fix guidance including the npm package and a manual binary path option.
|
||||
- **Explicit local provider**: checks for a local model file or a recognized
|
||||
remote/downloadable model URL. If missing, suggests switching to a remote provider.
|
||||
- **Explicit remote provider** (`openai`, `voyage`, etc.): verifies an API key is
|
||||
present in the environment or auth store. Prints actionable fix hints if missing.
|
||||
- **Auto provider**: checks local model availability first, then tries each remote
|
||||
provider in auto-selection order.
|
||||
|
||||
When a gateway probe result is available (gateway was healthy at the time of the
|
||||
check), doctor cross-references its result with the CLI-visible config and notes
|
||||
any discrepancy.
|
||||
|
||||
Use `openclaw memory status --deep` to verify embedding readiness at runtime.
|
||||
|
||||
### 14) Channel status warnings
|
||||
|
||||
If the gateway is healthy, doctor runs a channel status probe and reports
|
||||
warnings with suggested fixes.
|
||||
|
||||
### 15) Supervisor config audit + repair
|
||||
|
||||
Doctor checks the installed supervisor config (launchd/systemd/schtasks) for
|
||||
missing or outdated defaults (e.g., systemd network-online dependencies and
|
||||
restart delay). When it finds a mismatch, it recommends an update and can
|
||||
rewrite the service file/task to the current defaults.
|
||||
|
||||
Notes:
|
||||
|
||||
- `openclaw doctor` prompts before rewriting supervisor config.
|
||||
- `openclaw doctor --yes` accepts the default repair prompts.
|
||||
- `openclaw doctor --repair` applies recommended fixes without prompts.
|
||||
- `openclaw doctor --repair --force` overwrites custom supervisor configs.
|
||||
- If token auth requires a token and `gateway.auth.token` is SecretRef-managed, doctor service install/repair validates the SecretRef but does not persist resolved plaintext token values into supervisor service environment metadata.
|
||||
- If token auth requires a token and the configured token SecretRef is unresolved, doctor blocks the install/repair path with actionable guidance.
|
||||
- If both `gateway.auth.token` and `gateway.auth.password` are configured and `gateway.auth.mode` is unset, doctor blocks install/repair until mode is set explicitly.
|
||||
- For Linux user-systemd units, doctor token drift checks now include both `Environment=` and `EnvironmentFile=` sources when comparing service auth metadata.
|
||||
- You can always force a full rewrite via `openclaw gateway install --force`.
|
||||
|
||||
### 16) Gateway runtime + port diagnostics
|
||||
|
||||
Doctor inspects the service runtime (PID, last exit status) and warns when the
|
||||
service is installed but not actually running. It also checks for port collisions
|
||||
on the gateway port (default `18789`) and reports likely causes (gateway already
|
||||
running, SSH tunnel).
|
||||
|
||||
### 17) Gateway runtime best practices
|
||||
|
||||
Doctor warns when the gateway service runs on Bun or a version-managed Node path
|
||||
(`nvm`, `fnm`, `volta`, `asdf`, etc.). WhatsApp + Telegram channels require Node,
|
||||
and version-manager paths can break after upgrades because the service does not
|
||||
load your shell init. Doctor offers to migrate to a system Node install when
|
||||
available (Homebrew/apt/choco).
|
||||
|
||||
### 18) Config write + wizard metadata
|
||||
|
||||
Doctor persists any config changes and stamps wizard metadata to record the
|
||||
doctor run.
|
||||
|
||||
### 19) Workspace tips (backup + memory system)
|
||||
|
||||
Doctor suggests a workspace memory system when missing and prints a backup tip
|
||||
if the workspace is not already under git.
|
||||
|
||||
See [/concepts/agent-workspace](/concepts/agent-workspace) for a full guide to
|
||||
workspace structure and git backup (recommended private GitHub or GitLab).
|
||||
37
openclaw/docs/gateway/gateway-lock.md
Normal file
37
openclaw/docs/gateway/gateway-lock.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
summary: "Gateway singleton guard using the WebSocket listener bind"
|
||||
read_when:
|
||||
- Running or debugging the gateway process
|
||||
- Investigating single-instance enforcement
|
||||
title: "Gateway Lock"
|
||||
---
|
||||
|
||||
# Gateway lock
|
||||
|
||||
## Why
|
||||
|
||||
- Ensure only one gateway instance runs per base port on the same host; additional gateways must use isolated profiles and unique ports.
|
||||
- Survive crashes/SIGKILL without leaving stale lock files.
|
||||
- Fail fast with a clear error when the control port is already occupied.
|
||||
|
||||
## Mechanism
|
||||
|
||||
- The gateway binds the WebSocket listener (default `ws://127.0.0.1:18789`) immediately on startup using an exclusive TCP listener.
|
||||
- If the bind fails with `EADDRINUSE`, startup throws `GatewayLockError("another gateway instance is already listening on ws://127.0.0.1:<port>")`.
|
||||
- The OS releases the listener automatically on any process exit, including crashes and SIGKILL—no separate lock file or cleanup step is needed.
|
||||
- On shutdown the gateway closes the WebSocket server and underlying HTTP server to free the port promptly.
|
||||
|
||||
## Error surface
|
||||
|
||||
- If another process holds the port, startup throws `GatewayLockError("another gateway instance is already listening on ws://127.0.0.1:<port>")`.
|
||||
- Other bind failures surface as `GatewayLockError("failed to bind gateway socket on ws://127.0.0.1:<port>: …")`.
|
||||
|
||||
## Operational notes
|
||||
|
||||
- If the port is occupied by _another_ process, the error is the same; free the port or choose another with `openclaw gateway --port <port>`.
|
||||
- The macOS app still maintains its own lightweight PID guard before spawning the gateway; the runtime lock is enforced by the WebSocket bind.
|
||||
|
||||
## Related
|
||||
|
||||
- [Multiple Gateways](/gateway/multiple-gateways) — running multiple instances with unique ports
|
||||
- [Troubleshooting](/gateway/troubleshooting) — diagnosing `EADDRINUSE` and port conflicts
|
||||
61
openclaw/docs/gateway/health.md
Normal file
61
openclaw/docs/gateway/health.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
summary: "Health check commands and gateway health monitoring"
|
||||
read_when:
|
||||
- Diagnosing channel connectivity or gateway health
|
||||
- Understanding health check CLI commands and options
|
||||
title: "Health Checks"
|
||||
---
|
||||
|
||||
# Health Checks (CLI)
|
||||
|
||||
Short guide to verify channel connectivity without guessing.
|
||||
|
||||
## Quick checks
|
||||
|
||||
- `openclaw status` — local summary: gateway reachability/mode, update hint, linked channel auth age, sessions + recent activity.
|
||||
- `openclaw status --all` — full local diagnosis (read-only, color, safe to paste for debugging).
|
||||
- `openclaw status --deep` — asks the running gateway for a live health probe (`health` with `probe:true`), including per-account channel probes when supported.
|
||||
- `openclaw health` — asks the running gateway for its health snapshot (WS-only; no direct channel sockets from the CLI).
|
||||
- `openclaw health --verbose` — forces a live health probe and prints gateway connection details.
|
||||
- `openclaw health --json` — machine-readable health snapshot output.
|
||||
- Send `/status` as a standalone message in WhatsApp/WebChat to get a status reply without invoking the agent.
|
||||
- Logs: tail `/tmp/openclaw/openclaw-*.log` and filter for `web-heartbeat`, `web-reconnect`, `web-auto-reply`, `web-inbound`.
|
||||
|
||||
## Deep diagnostics
|
||||
|
||||
- Creds on disk: `ls -l ~/.openclaw/credentials/whatsapp/<accountId>/creds.json` (mtime should be recent).
|
||||
- Session store: `ls -l ~/.openclaw/agents/<agentId>/sessions/sessions.json` (path can be overridden in config). Count and recent recipients are surfaced via `status`.
|
||||
- Relink flow: `openclaw channels logout && openclaw channels login --verbose` when status codes 409–515 or `loggedOut` appear in logs. (Note: the QR login flow auto-restarts once for status 515 after pairing.)
|
||||
|
||||
## Health monitor config
|
||||
|
||||
- `gateway.channelHealthCheckMinutes`: how often the gateway checks channel health. Default: `5`. Set `0` to disable health-monitor restarts globally.
|
||||
- `gateway.channelStaleEventThresholdMinutes`: how long a connected channel can stay idle before the health monitor treats it as stale and restarts it. Default: `30`. Keep this greater than or equal to `gateway.channelHealthCheckMinutes`.
|
||||
- `gateway.channelMaxRestartsPerHour`: rolling one-hour cap for health-monitor restarts per channel/account. Default: `10`.
|
||||
- `channels.<provider>.healthMonitor.enabled`: disable health-monitor restarts for a specific channel while leaving global monitoring enabled.
|
||||
- `channels.<provider>.accounts.<accountId>.healthMonitor.enabled`: multi-account override that wins over the channel-level setting.
|
||||
- These per-channel overrides apply to the built-in channel monitors that expose them today: Discord, Google Chat, iMessage, Microsoft Teams, Signal, Slack, Telegram, and WhatsApp.
|
||||
|
||||
## When something fails
|
||||
|
||||
- `logged out` or status 409–515 → relink with `openclaw channels logout` then `openclaw channels login`.
|
||||
- Gateway unreachable → start it: `openclaw gateway --port 18789` (use `--force` if the port is busy).
|
||||
- No inbound messages → confirm linked phone is online and the sender is allowed (`channels.whatsapp.allowFrom`); for group chats, ensure allowlist + mention rules match (`channels.whatsapp.groups`, `agents.list[].groupChat.mentionPatterns`).
|
||||
|
||||
## Dedicated "health" command
|
||||
|
||||
`openclaw health` asks the running gateway for its health snapshot (no direct channel
|
||||
sockets from the CLI). By default it can return a fresh cached gateway snapshot; the
|
||||
gateway then refreshes that cache in the background. `openclaw health --verbose` forces
|
||||
a live probe instead. The command reports linked creds/auth age when available,
|
||||
per-channel probe summaries, session-store summary, and a probe duration. It exits
|
||||
non-zero if the gateway is unreachable or the probe fails/timeouts.
|
||||
|
||||
Options:
|
||||
|
||||
- `--json`: machine-readable JSON output
|
||||
- `--timeout <ms>`: override the default 10s probe timeout
|
||||
- `--verbose`: force a live probe and print gateway connection details
|
||||
- `--debug`: alias for `--verbose`
|
||||
|
||||
The health snapshot includes: `ok` (boolean), `ts` (timestamp), `durationMs` (probe time), per-channel status, agent availability, and session-store summary.
|
||||
452
openclaw/docs/gateway/heartbeat.md
Normal file
452
openclaw/docs/gateway/heartbeat.md
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
---
|
||||
summary: "Heartbeat polling messages and notification rules"
|
||||
read_when:
|
||||
- Adjusting heartbeat cadence or messaging
|
||||
- Deciding between heartbeat and cron for scheduled tasks
|
||||
title: "Heartbeat"
|
||||
---
|
||||
|
||||
# Heartbeat (Gateway)
|
||||
|
||||
> **Heartbeat vs Cron?** See [Automation & Tasks](/automation) for guidance on when to use each.
|
||||
|
||||
Heartbeat runs **periodic agent turns** in the main session so the model can
|
||||
surface anything that needs attention without spamming you.
|
||||
|
||||
Heartbeat is a scheduled main-session turn — it does **not** create [background task](/automation/tasks) records.
|
||||
Task records are for detached work (ACP runs, subagents, isolated cron jobs).
|
||||
|
||||
Troubleshooting: [Scheduled Tasks](/automation/cron-jobs#troubleshooting)
|
||||
|
||||
## Quick start (beginner)
|
||||
|
||||
1. Leave heartbeats enabled (default is `30m`, or `1h` for Anthropic OAuth/token auth, including Claude CLI reuse) or set your own cadence.
|
||||
2. Create a tiny `HEARTBEAT.md` checklist or `tasks:` block in the agent workspace (optional but recommended).
|
||||
3. Decide where heartbeat messages should go (`target: "none"` is the default; set `target: "last"` to route to the last contact).
|
||||
4. Optional: enable heartbeat reasoning delivery for transparency.
|
||||
5. Optional: use lightweight bootstrap context if heartbeat runs only need `HEARTBEAT.md`.
|
||||
6. Optional: enable isolated sessions to avoid sending full conversation history each heartbeat.
|
||||
7. Optional: restrict heartbeats to active hours (local time).
|
||||
|
||||
Example config:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
every: "30m",
|
||||
target: "last", // explicit delivery to last contact (default is "none")
|
||||
directPolicy: "allow", // default: allow direct/DM targets; set "block" to suppress
|
||||
lightContext: true, // optional: only inject HEARTBEAT.md from bootstrap files
|
||||
isolatedSession: true, // optional: fresh session each run (no conversation history)
|
||||
// activeHours: { start: "08:00", end: "24:00" },
|
||||
// includeReasoning: true, // optional: send separate `Reasoning:` message too
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Defaults
|
||||
|
||||
- Interval: `30m` (or `1h` when Anthropic OAuth/token auth is the detected auth mode, including Claude CLI reuse). Set `agents.defaults.heartbeat.every` or per-agent `agents.list[].heartbeat.every`; use `0m` to disable.
|
||||
- Prompt body (configurable via `agents.defaults.heartbeat.prompt`):
|
||||
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
||||
- The heartbeat prompt is sent **verbatim** as the user message. The system
|
||||
prompt includes a “Heartbeat” section only when heartbeats are enabled for the
|
||||
default agent, and the run is flagged internally.
|
||||
- When heartbeats are disabled with `0m`, normal runs also omit `HEARTBEAT.md`
|
||||
from bootstrap context so the model does not see heartbeat-only instructions.
|
||||
- Active hours (`heartbeat.activeHours`) are checked in the configured timezone.
|
||||
Outside the window, heartbeats are skipped until the next tick inside the window.
|
||||
|
||||
## What the heartbeat prompt is for
|
||||
|
||||
The default prompt is intentionally broad:
|
||||
|
||||
- **Background tasks**: “Consider outstanding tasks” nudges the agent to review
|
||||
follow-ups (inbox, calendar, reminders, queued work) and surface anything urgent.
|
||||
- **Human check-in**: “Checkup sometimes on your human during day time” nudges an
|
||||
occasional lightweight “anything you need?” message, but avoids night-time spam
|
||||
by using your configured local timezone (see [/concepts/timezone](/concepts/timezone)).
|
||||
|
||||
Heartbeat can react to completed [background tasks](/automation/tasks), but a heartbeat run itself does not create a task record.
|
||||
|
||||
If you want a heartbeat to do something very specific (e.g. “check Gmail PubSub
|
||||
stats” or “verify gateway health”), set `agents.defaults.heartbeat.prompt` (or
|
||||
`agents.list[].heartbeat.prompt`) to a custom body (sent verbatim).
|
||||
|
||||
## Response contract
|
||||
|
||||
- If nothing needs attention, reply with **`HEARTBEAT_OK`**.
|
||||
- During heartbeat runs, OpenClaw treats `HEARTBEAT_OK` as an ack when it appears
|
||||
at the **start or end** of the reply. The token is stripped and the reply is
|
||||
dropped if the remaining content is **≤ `ackMaxChars`** (default: 300).
|
||||
- If `HEARTBEAT_OK` appears in the **middle** of a reply, it is not treated
|
||||
specially.
|
||||
- For alerts, **do not** include `HEARTBEAT_OK`; return only the alert text.
|
||||
|
||||
Outside heartbeats, stray `HEARTBEAT_OK` at the start/end of a message is stripped
|
||||
and logged; a message that is only `HEARTBEAT_OK` is dropped.
|
||||
|
||||
## Config
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
every: "30m", // default: 30m (0m disables)
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
|
||||
lightContext: false, // default: false; true keeps only HEARTBEAT.md from workspace bootstrap files
|
||||
isolatedSession: false, // default: false; true runs each heartbeat in a fresh session (no conversation history)
|
||||
target: "last", // default: none | options: last | none | <channel id> (core or plugin, e.g. "bluebubbles")
|
||||
to: "+15551234567", // optional channel-specific override
|
||||
accountId: "ops-bot", // optional multi-account channel id
|
||||
prompt: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
|
||||
ackMaxChars: 300, // max chars allowed after HEARTBEAT_OK
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Scope and precedence
|
||||
|
||||
- `agents.defaults.heartbeat` sets global heartbeat behavior.
|
||||
- `agents.list[].heartbeat` merges on top; if any agent has a `heartbeat` block, **only those agents** run heartbeats.
|
||||
- `channels.defaults.heartbeat` sets visibility defaults for all channels.
|
||||
- `channels.<channel>.heartbeat` overrides channel defaults.
|
||||
- `channels.<channel>.accounts.<id>.heartbeat` (multi-account channels) overrides per-channel settings.
|
||||
|
||||
### Per-agent heartbeats
|
||||
|
||||
If any `agents.list[]` entry includes a `heartbeat` block, **only those agents**
|
||||
run heartbeats. The per-agent block merges on top of `agents.defaults.heartbeat`
|
||||
(so you can set shared defaults once and override per agent).
|
||||
|
||||
Example: two agents, only the second agent runs heartbeats.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
every: "30m",
|
||||
target: "last", // explicit delivery to last contact (default is "none")
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{ id: "main", default: true },
|
||||
{
|
||||
id: "ops",
|
||||
heartbeat: {
|
||||
every: "1h",
|
||||
target: "whatsapp",
|
||||
to: "+15551234567",
|
||||
timeoutSeconds: 45,
|
||||
prompt: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Active hours example
|
||||
|
||||
Restrict heartbeats to business hours in a specific timezone:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
heartbeat: {
|
||||
every: "30m",
|
||||
target: "last", // explicit delivery to last contact (default is "none")
|
||||
activeHours: {
|
||||
start: "09:00",
|
||||
end: "22:00",
|
||||
timezone: "America/New_York", // optional; uses your userTimezone if set, otherwise host tz
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Outside this window (before 9am or after 10pm Eastern), heartbeats are skipped. The next scheduled tick inside the window will run normally.
|
||||
|
||||
### 24/7 setup
|
||||
|
||||
If you want heartbeats to run all day, use one of these patterns:
|
||||
|
||||
- Omit `activeHours` entirely (no time-window restriction; this is the default behavior).
|
||||
- Set a full-day window: `activeHours: { start: "00:00", end: "24:00" }`.
|
||||
|
||||
Do not set the same `start` and `end` time (for example `08:00` to `08:00`).
|
||||
That is treated as a zero-width window, so heartbeats are always skipped.
|
||||
|
||||
### Multi account example
|
||||
|
||||
Use `accountId` to target a specific account on multi-account channels like Telegram:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "ops",
|
||||
heartbeat: {
|
||||
every: "1h",
|
||||
target: "telegram",
|
||||
to: "12345678:topic:42", // optional: route to a specific topic/thread
|
||||
accountId: "ops-bot",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
"ops-bot": { botToken: "YOUR_TELEGRAM_BOT_TOKEN" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Field notes
|
||||
|
||||
- `every`: heartbeat interval (duration string; default unit = minutes).
|
||||
- `model`: optional model override for heartbeat runs (`provider/model`).
|
||||
- `includeReasoning`: when enabled, also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`).
|
||||
- `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
|
||||
- `isolatedSession`: when true, each heartbeat runs in a fresh session with no prior conversation history. Uses the same isolation pattern as cron `sessionTarget: "isolated"`. Dramatically reduces per-heartbeat token cost. Combine with `lightContext: true` for maximum savings. Delivery routing still uses the main session context.
|
||||
- `session`: optional session key for heartbeat runs.
|
||||
- `main` (default): agent main session.
|
||||
- Explicit session key (copy from `openclaw sessions --json` or the [sessions CLI](/cli/sessions)).
|
||||
- Session key formats: see [Sessions](/concepts/session) and [Groups](/channels/groups).
|
||||
- `target`:
|
||||
- `last`: deliver to the last used external channel.
|
||||
- explicit channel: any configured channel or plugin id, for example `discord`, `matrix`, `telegram`, or `whatsapp`.
|
||||
- `none` (default): run the heartbeat but **do not deliver** externally.
|
||||
- `directPolicy`: controls direct/DM delivery behavior:
|
||||
- `allow` (default): allow direct/DM heartbeat delivery.
|
||||
- `block`: suppress direct/DM delivery (`reason=dm-blocked`).
|
||||
- `to`: optional recipient override (channel-specific id, e.g. E.164 for WhatsApp or a Telegram chat id). For Telegram topics/threads, use `<chatId>:topic:<messageThreadId>`.
|
||||
- `accountId`: optional account id for multi-account channels. When `target: "last"`, the account id applies to the resolved last channel if it supports accounts; otherwise it is ignored. If the account id does not match a configured account for the resolved channel, delivery is skipped.
|
||||
- `prompt`: overrides the default prompt body (not merged).
|
||||
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery.
|
||||
- `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
|
||||
- `activeHours`: restricts heartbeat runs to a time window. Object with `start` (HH:MM, inclusive; use `00:00` for start-of-day), `end` (HH:MM exclusive; `24:00` allowed for end-of-day), and optional `timezone`.
|
||||
- Omitted or `"user"`: uses your `agents.defaults.userTimezone` if set, otherwise falls back to the host system timezone.
|
||||
- `"local"`: always uses the host system timezone.
|
||||
- Any IANA identifier (e.g. `America/New_York`): used directly; if invalid, falls back to the `"user"` behavior above.
|
||||
- `start` and `end` must not be equal for an active window; equal values are treated as zero-width (always outside the window).
|
||||
- Outside the active window, heartbeats are skipped until the next tick inside the window.
|
||||
|
||||
## Delivery behavior
|
||||
|
||||
- Heartbeats run in the agent’s main session by default (`agent:<id>:<mainKey>`),
|
||||
or `global` when `session.scope = "global"`. Set `session` to override to a
|
||||
specific channel session (Discord/WhatsApp/etc.).
|
||||
- `session` only affects the run context; delivery is controlled by `target` and `to`.
|
||||
- To deliver to a specific channel/recipient, set `target` + `to`. With
|
||||
`target: "last"`, delivery uses the last external channel for that session.
|
||||
- Heartbeat deliveries allow direct/DM targets by default. Set `directPolicy: "block"` to suppress direct-target sends while still running the heartbeat turn.
|
||||
- If the main queue is busy, the heartbeat is skipped and retried later.
|
||||
- If `target` resolves to no external destination, the run still happens but no
|
||||
outbound message is sent.
|
||||
- If `showOk`, `showAlerts`, and `useIndicator` are all disabled, the run is skipped up front as `reason=alerts-disabled`.
|
||||
- If only alert delivery is disabled, OpenClaw can still run the heartbeat, update due-task timestamps, restore the session idle timestamp, and suppress the outward alert payload.
|
||||
- Heartbeat-only replies do **not** keep the session alive; the last `updatedAt`
|
||||
is restored so idle expiry behaves normally.
|
||||
- Detached [background tasks](/automation/tasks) can enqueue a system event and wake heartbeat when the main session should notice something quickly. That wake does not make the heartbeat run a background task.
|
||||
|
||||
## Visibility controls
|
||||
|
||||
By default, `HEARTBEAT_OK` acknowledgments are suppressed while alert content is
|
||||
delivered. You can adjust this per channel or per account:
|
||||
|
||||
```yaml
|
||||
channels:
|
||||
defaults:
|
||||
heartbeat:
|
||||
showOk: false # Hide HEARTBEAT_OK (default)
|
||||
showAlerts: true # Show alert messages (default)
|
||||
useIndicator: true # Emit indicator events (default)
|
||||
telegram:
|
||||
heartbeat:
|
||||
showOk: true # Show OK acknowledgments on Telegram
|
||||
whatsapp:
|
||||
accounts:
|
||||
work:
|
||||
heartbeat:
|
||||
showAlerts: false # Suppress alert delivery for this account
|
||||
```
|
||||
|
||||
Precedence: per-account → per-channel → channel defaults → built-in defaults.
|
||||
|
||||
### What each flag does
|
||||
|
||||
- `showOk`: sends a `HEARTBEAT_OK` acknowledgment when the model returns an OK-only reply.
|
||||
- `showAlerts`: sends the alert content when the model returns a non-OK reply.
|
||||
- `useIndicator`: emits indicator events for UI status surfaces.
|
||||
|
||||
If **all three** are false, OpenClaw skips the heartbeat run entirely (no model call).
|
||||
|
||||
### Per-channel vs per-account examples
|
||||
|
||||
```yaml
|
||||
channels:
|
||||
defaults:
|
||||
heartbeat:
|
||||
showOk: false
|
||||
showAlerts: true
|
||||
useIndicator: true
|
||||
slack:
|
||||
heartbeat:
|
||||
showOk: true # all Slack accounts
|
||||
accounts:
|
||||
ops:
|
||||
heartbeat:
|
||||
showAlerts: false # suppress alerts for the ops account only
|
||||
telegram:
|
||||
heartbeat:
|
||||
showOk: true
|
||||
```
|
||||
|
||||
### Common patterns
|
||||
|
||||
| Goal | Config |
|
||||
| ---------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| Default behavior (silent OKs, alerts on) | _(no config needed)_ |
|
||||
| Fully silent (no messages, no indicator) | `channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: false }` |
|
||||
| Indicator-only (no messages) | `channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: true }` |
|
||||
| OKs in one channel only | `channels.telegram.heartbeat: { showOk: true }` |
|
||||
|
||||
## HEARTBEAT.md (optional)
|
||||
|
||||
If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
|
||||
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
|
||||
safe to include every 30 minutes.
|
||||
|
||||
On normal runs, `HEARTBEAT.md` is only injected when heartbeat guidance is
|
||||
enabled for the default agent. Disabling the heartbeat cadence with `0m` or
|
||||
setting `includeSystemPromptSection: false` omits it from normal bootstrap
|
||||
context.
|
||||
|
||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
|
||||
headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls.
|
||||
That skip is reported as `reason=empty-heartbeat-file`.
|
||||
If the file is missing, the heartbeat still runs and the model decides what to do.
|
||||
|
||||
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
|
||||
|
||||
Example `HEARTBEAT.md`:
|
||||
|
||||
```md
|
||||
# Heartbeat checklist
|
||||
|
||||
- Quick scan: anything urgent in inboxes?
|
||||
- If it’s daytime, do a lightweight check-in if nothing else is pending.
|
||||
- If a task is blocked, write down _what is missing_ and ask Peter next time.
|
||||
```
|
||||
|
||||
### `tasks:` blocks
|
||||
|
||||
`HEARTBEAT.md` also supports a small structured `tasks:` block for interval-based
|
||||
checks inside heartbeat itself.
|
||||
|
||||
Example:
|
||||
|
||||
```md
|
||||
tasks:
|
||||
|
||||
- name: inbox-triage
|
||||
interval: 30m
|
||||
prompt: "Check for urgent unread emails and flag anything time sensitive."
|
||||
- name: calendar-scan
|
||||
interval: 2h
|
||||
prompt: "Check for upcoming meetings that need prep or follow-up."
|
||||
|
||||
# Additional instructions
|
||||
|
||||
- Keep alerts short.
|
||||
- If nothing needs attention after all due tasks, reply HEARTBEAT_OK.
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- OpenClaw parses the `tasks:` block and checks each task against its own `interval`.
|
||||
- Only **due** tasks are included in the heartbeat prompt for that tick.
|
||||
- If no tasks are due, the heartbeat is skipped entirely (`reason=no-tasks-due`) to avoid a wasted model call.
|
||||
- Non-task content in `HEARTBEAT.md` is preserved and appended as additional context after the due-task list.
|
||||
- Task last-run timestamps are stored in session state (`heartbeatTaskState`), so intervals survive normal restarts.
|
||||
- Task timestamps are only advanced after a heartbeat run completes its normal reply path. Skipped `empty-heartbeat-file` / `no-tasks-due` runs do not mark tasks as completed.
|
||||
|
||||
Task mode is useful when you want one heartbeat file to hold several periodic checks without paying for all of them every tick.
|
||||
|
||||
### Can the agent update HEARTBEAT.md?
|
||||
|
||||
Yes — if you ask it to.
|
||||
|
||||
`HEARTBEAT.md` is just a normal file in the agent workspace, so you can tell the
|
||||
agent (in a normal chat) something like:
|
||||
|
||||
- “Update `HEARTBEAT.md` to add a daily calendar check.”
|
||||
- “Rewrite `HEARTBEAT.md` so it’s shorter and focused on inbox follow-ups.”
|
||||
|
||||
If you want this to happen proactively, you can also include an explicit line in
|
||||
your heartbeat prompt like: “If the checklist becomes stale, update HEARTBEAT.md
|
||||
with a better one.”
|
||||
|
||||
Safety note: don’t put secrets (API keys, phone numbers, private tokens) into
|
||||
`HEARTBEAT.md` — it becomes part of the prompt context.
|
||||
|
||||
## Manual wake (on-demand)
|
||||
|
||||
You can enqueue a system event and trigger an immediate heartbeat with:
|
||||
|
||||
```bash
|
||||
openclaw system event --text "Check for urgent follow-ups" --mode now
|
||||
```
|
||||
|
||||
If multiple agents have `heartbeat` configured, a manual wake runs each of those
|
||||
agent heartbeats immediately.
|
||||
|
||||
Use `--mode next-heartbeat` to wait for the next scheduled tick.
|
||||
|
||||
## Reasoning delivery (optional)
|
||||
|
||||
By default, heartbeats deliver only the final “answer” payload.
|
||||
|
||||
If you want transparency, enable:
|
||||
|
||||
- `agents.defaults.heartbeat.includeReasoning: true`
|
||||
|
||||
When enabled, heartbeats will also deliver a separate message prefixed
|
||||
`Reasoning:` (same shape as `/reasoning on`). This can be useful when the agent
|
||||
is managing multiple sessions/codexes and you want to see why it decided to ping
|
||||
you — but it can also leak more internal detail than you want. Prefer keeping it
|
||||
off in group chats.
|
||||
|
||||
## Cost awareness
|
||||
|
||||
Heartbeats run full agent turns. Shorter intervals burn more tokens. To reduce cost:
|
||||
|
||||
- Use `isolatedSession: true` to avoid sending full conversation history (~100K tokens down to ~2-5K per run).
|
||||
- Use `lightContext: true` to limit bootstrap files to just `HEARTBEAT.md`.
|
||||
- Set a cheaper `model` (e.g. `ollama/llama3.2:1b`).
|
||||
- Keep `HEARTBEAT.md` small.
|
||||
- Use `target: "none"` if you only want internal state updates.
|
||||
|
||||
## Related
|
||||
|
||||
- [Automation & Tasks](/automation) — all automation mechanisms at a glance
|
||||
- [Background Tasks](/automation/tasks) — how detached work is tracked
|
||||
- [Timezone](/concepts/timezone) — how timezone affects heartbeat scheduling
|
||||
- [Troubleshooting](/automation/cron-jobs#troubleshooting) — debugging automation issues
|
||||
367
openclaw/docs/gateway/index.md
Normal file
367
openclaw/docs/gateway/index.md
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
---
|
||||
summary: "Runbook for the Gateway service, lifecycle, and operations"
|
||||
read_when:
|
||||
- Running or debugging the gateway process
|
||||
title: "Gateway Runbook"
|
||||
---
|
||||
|
||||
# Gateway runbook
|
||||
|
||||
Use this page for day-1 startup and day-2 operations of the Gateway service.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Deep troubleshooting" icon="siren" href="/gateway/troubleshooting">
|
||||
Symptom-first diagnostics with exact command ladders and log signatures.
|
||||
</Card>
|
||||
<Card title="Configuration" icon="sliders" href="/gateway/configuration">
|
||||
Task-oriented setup guide + full configuration reference.
|
||||
</Card>
|
||||
<Card title="Secrets management" icon="key-round" href="/gateway/secrets">
|
||||
SecretRef contract, runtime snapshot behavior, and migrate/reload operations.
|
||||
</Card>
|
||||
<Card title="Secrets plan contract" icon="shield-check" href="/gateway/secrets-plan-contract">
|
||||
Exact `secrets apply` target/path rules and ref-only auth-profile behavior.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 5-minute local startup
|
||||
|
||||
<Steps>
|
||||
<Step title="Start the Gateway">
|
||||
|
||||
```bash
|
||||
openclaw gateway --port 18789
|
||||
# debug/trace mirrored to stdio
|
||||
openclaw gateway --port 18789 --verbose
|
||||
# force-kill listener on selected port, then start
|
||||
openclaw gateway --force
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Verify service health">
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
openclaw status
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
Healthy baseline: `Runtime: running`, `Connectivity probe: ok`, and `Capability: ...` that matches what you expect. Use `openclaw gateway status --require-rpc` when you need read-scope RPC proof, not just reachability.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Validate channel readiness">
|
||||
|
||||
```bash
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
With a reachable gateway this runs live per-account channel probes and optional audits.
|
||||
If the gateway is unreachable, the CLI falls back to config-only channel summaries instead
|
||||
of live probe output.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
Gateway config reload watches the active config file path (resolved from profile/state defaults, or `OPENCLAW_CONFIG_PATH` when set).
|
||||
Default mode is `gateway.reload.mode="hybrid"`.
|
||||
After the first successful load, the running process serves the active in-memory config snapshot; successful reload swaps that snapshot atomically.
|
||||
</Note>
|
||||
|
||||
## Runtime model
|
||||
|
||||
- One always-on process for routing, control plane, and channel connections.
|
||||
- Single multiplexed port for:
|
||||
- WebSocket control/RPC
|
||||
- HTTP APIs, OpenAI compatible (`/v1/models`, `/v1/embeddings`, `/v1/chat/completions`, `/v1/responses`, `/tools/invoke`)
|
||||
- Control UI and hooks
|
||||
- Default bind mode: `loopback`.
|
||||
- Auth is required by default. Shared-secret setups use
|
||||
`gateway.auth.token` / `gateway.auth.password` (or
|
||||
`OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`), and non-loopback
|
||||
reverse-proxy setups can use `gateway.auth.mode: "trusted-proxy"`.
|
||||
|
||||
## OpenAI-compatible endpoints
|
||||
|
||||
OpenClaw’s highest-leverage compatibility surface is now:
|
||||
|
||||
- `GET /v1/models`
|
||||
- `GET /v1/models/{id}`
|
||||
- `POST /v1/embeddings`
|
||||
- `POST /v1/chat/completions`
|
||||
- `POST /v1/responses`
|
||||
|
||||
Why this set matters:
|
||||
|
||||
- Most Open WebUI, LobeChat, and LibreChat integrations probe `/v1/models` first.
|
||||
- Many RAG and memory pipelines expect `/v1/embeddings`.
|
||||
- Agent-native clients increasingly prefer `/v1/responses`.
|
||||
|
||||
Planning note:
|
||||
|
||||
- `/v1/models` is agent-first: it returns `openclaw`, `openclaw/default`, and `openclaw/<agentId>`.
|
||||
- `openclaw/default` is the stable alias that always maps to the configured default agent.
|
||||
- Use `x-openclaw-model` when you want a backend provider/model override; otherwise the selected agent's normal model and embedding setup stays in control.
|
||||
|
||||
All of these run on the main Gateway port and use the same trusted operator auth boundary as the rest of the Gateway HTTP API.
|
||||
|
||||
### Port and bind precedence
|
||||
|
||||
| Setting | Resolution order |
|
||||
| ------------ | ------------------------------------------------------------- |
|
||||
| Gateway port | `--port` → `OPENCLAW_GATEWAY_PORT` → `gateway.port` → `18789` |
|
||||
| Bind mode | CLI/override → `gateway.bind` → `loopback` |
|
||||
|
||||
### Hot reload modes
|
||||
|
||||
| `gateway.reload.mode` | Behavior |
|
||||
| --------------------- | ------------------------------------------ |
|
||||
| `off` | No config reload |
|
||||
| `hot` | Apply only hot-safe changes |
|
||||
| `restart` | Restart on reload-required changes |
|
||||
| `hybrid` (default) | Hot-apply when safe, restart when required |
|
||||
|
||||
## Operator command set
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
openclaw gateway status --deep # adds a system-level service scan
|
||||
openclaw gateway status --json
|
||||
openclaw gateway install
|
||||
openclaw gateway restart
|
||||
openclaw gateway stop
|
||||
openclaw secrets reload
|
||||
openclaw logs --follow
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
`gateway status --deep` is for extra service discovery (LaunchDaemons/systemd system
|
||||
units/schtasks), not a deeper RPC health probe.
|
||||
|
||||
## Multiple gateways (same host)
|
||||
|
||||
Most installs should run one gateway per machine. A single gateway can host multiple
|
||||
agents and channels.
|
||||
|
||||
You only need multiple gateways when you intentionally want isolation or a rescue bot.
|
||||
|
||||
Useful checks:
|
||||
|
||||
```bash
|
||||
openclaw gateway status --deep
|
||||
openclaw gateway probe
|
||||
```
|
||||
|
||||
What to expect:
|
||||
|
||||
- `gateway status --deep` can report `Other gateway-like services detected (best effort)`
|
||||
and print cleanup hints when stale launchd/systemd/schtasks installs are still around.
|
||||
- `gateway probe` can warn about `multiple reachable gateways` when more than one target
|
||||
answers.
|
||||
- If that is intentional, isolate ports, config/state, and workspace roots per gateway.
|
||||
|
||||
Detailed setup: [/gateway/multiple-gateways](/gateway/multiple-gateways).
|
||||
|
||||
## Remote access
|
||||
|
||||
Preferred: Tailscale/VPN.
|
||||
Fallback: SSH tunnel.
|
||||
|
||||
```bash
|
||||
ssh -N -L 18789:127.0.0.1:18789 user@host
|
||||
```
|
||||
|
||||
Then connect clients locally to `ws://127.0.0.1:18789`.
|
||||
|
||||
<Warning>
|
||||
SSH tunnels do not bypass gateway auth. For shared-secret auth, clients still
|
||||
must send `token`/`password` even over the tunnel. For identity-bearing modes,
|
||||
the request still has to satisfy that auth path.
|
||||
</Warning>
|
||||
|
||||
See: [Remote Gateway](/gateway/remote), [Authentication](/gateway/authentication), [Tailscale](/gateway/tailscale).
|
||||
|
||||
## Supervision and service lifecycle
|
||||
|
||||
Use supervised runs for production-like reliability.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="macOS (launchd)">
|
||||
|
||||
```bash
|
||||
openclaw gateway install
|
||||
openclaw gateway status
|
||||
openclaw gateway restart
|
||||
openclaw gateway stop
|
||||
```
|
||||
|
||||
LaunchAgent labels are `ai.openclaw.gateway` (default) or `ai.openclaw.<profile>` (named profile). `openclaw doctor` audits and repairs service config drift.
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Linux (systemd user)">
|
||||
|
||||
```bash
|
||||
openclaw gateway install
|
||||
systemctl --user enable --now openclaw-gateway[-<profile>].service
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
For persistence after logout, enable lingering:
|
||||
|
||||
```bash
|
||||
sudo loginctl enable-linger <user>
|
||||
```
|
||||
|
||||
Manual user-unit example when you need a custom install path:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=OpenClaw Gateway
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/openclaw gateway --port 18789
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStopSec=30
|
||||
TimeoutStartSec=30
|
||||
SuccessExitStatus=0 143
|
||||
KillMode=control-group
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Windows (native)">
|
||||
|
||||
```powershell
|
||||
openclaw gateway install
|
||||
openclaw gateway status --json
|
||||
openclaw gateway restart
|
||||
openclaw gateway stop
|
||||
```
|
||||
|
||||
Native Windows managed startup uses a Scheduled Task named `OpenClaw Gateway`
|
||||
(or `OpenClaw Gateway (<profile>)` for named profiles). If Scheduled Task
|
||||
creation is denied, OpenClaw falls back to a per-user Startup-folder launcher
|
||||
that points at `gateway.cmd` inside the state directory.
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Linux (system service)">
|
||||
|
||||
Use a system unit for multi-user/always-on hosts.
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now openclaw-gateway[-<profile>].service
|
||||
```
|
||||
|
||||
Use the same service body as the user unit, but install it under
|
||||
`/etc/systemd/system/openclaw-gateway[-<profile>].service` and adjust
|
||||
`ExecStart=` if your `openclaw` binary lives elsewhere.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Multiple gateways on one host
|
||||
|
||||
Most setups should run **one** Gateway.
|
||||
Use multiple only for strict isolation/redundancy (for example a rescue profile).
|
||||
|
||||
Checklist per instance:
|
||||
|
||||
- Unique `gateway.port`
|
||||
- Unique `OPENCLAW_CONFIG_PATH`
|
||||
- Unique `OPENCLAW_STATE_DIR`
|
||||
- Unique `agents.defaults.workspace`
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
OPENCLAW_CONFIG_PATH=~/.openclaw/a.json OPENCLAW_STATE_DIR=~/.openclaw-a openclaw gateway --port 19001
|
||||
OPENCLAW_CONFIG_PATH=~/.openclaw/b.json OPENCLAW_STATE_DIR=~/.openclaw-b openclaw gateway --port 19002
|
||||
```
|
||||
|
||||
See: [Multiple gateways](/gateway/multiple-gateways).
|
||||
|
||||
### Dev profile quick path
|
||||
|
||||
```bash
|
||||
openclaw --dev setup
|
||||
openclaw --dev gateway --allow-unconfigured
|
||||
openclaw --dev status
|
||||
```
|
||||
|
||||
Defaults include isolated state/config and base gateway port `19001`.
|
||||
|
||||
## Protocol quick reference (operator view)
|
||||
|
||||
- First client frame must be `connect`.
|
||||
- Gateway returns `hello-ok` snapshot (`presence`, `health`, `stateVersion`, `uptimeMs`, limits/policy).
|
||||
- `hello-ok.features.methods` / `events` are a conservative discovery list, not
|
||||
a generated dump of every callable helper route.
|
||||
- Requests: `req(method, params)` → `res(ok/payload|error)`.
|
||||
- Common events include `connect.challenge`, `agent`, `chat`,
|
||||
`session.message`, `session.tool`, `sessions.changed`, `presence`, `tick`,
|
||||
`health`, `heartbeat`, pairing/approval lifecycle events, and `shutdown`.
|
||||
|
||||
Agent runs are two-stage:
|
||||
|
||||
1. Immediate accepted ack (`status:"accepted"`)
|
||||
2. Final completion response (`status:"ok"|"error"`), with streamed `agent` events in between.
|
||||
|
||||
See full protocol docs: [Gateway Protocol](/gateway/protocol).
|
||||
|
||||
## Operational checks
|
||||
|
||||
### Liveness
|
||||
|
||||
- Open WS and send `connect`.
|
||||
- Expect `hello-ok` response with snapshot.
|
||||
|
||||
### Readiness
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
openclaw channels status --probe
|
||||
openclaw health
|
||||
```
|
||||
|
||||
### Gap recovery
|
||||
|
||||
Events are not replayed. On sequence gaps, refresh state (`health`, `system-presence`) before continuing.
|
||||
|
||||
## Common failure signatures
|
||||
|
||||
| Signature | Likely issue |
|
||||
| -------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `refusing to bind gateway ... without auth` | Non-loopback bind without a valid gateway auth path |
|
||||
| `another gateway instance is already listening` / `EADDRINUSE` | Port conflict |
|
||||
| `Gateway start blocked: set gateway.mode=local` | Config set to remote mode, or local-mode stamp is missing from a damaged config |
|
||||
| `unauthorized` during connect | Auth mismatch between client and gateway |
|
||||
|
||||
For full diagnosis ladders, use [Gateway Troubleshooting](/gateway/troubleshooting).
|
||||
|
||||
## Safety guarantees
|
||||
|
||||
- Gateway protocol clients fail fast when Gateway is unavailable (no implicit direct-channel fallback).
|
||||
- Invalid/non-connect first frames are rejected and closed.
|
||||
- Graceful shutdown emits `shutdown` event before socket close.
|
||||
|
||||
---
|
||||
|
||||
Related:
|
||||
|
||||
- [Troubleshooting](/gateway/troubleshooting)
|
||||
- [Background Process](/gateway/background-process)
|
||||
- [Configuration](/gateway/configuration)
|
||||
- [Health](/gateway/health)
|
||||
- [Doctor](/gateway/doctor)
|
||||
- [Authentication](/gateway/authentication)
|
||||
189
openclaw/docs/gateway/local-models.md
Normal file
189
openclaw/docs/gateway/local-models.md
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
---
|
||||
summary: "Run OpenClaw on local LLMs (LM Studio, vLLM, LiteLLM, custom OpenAI endpoints)"
|
||||
read_when:
|
||||
- You want to serve models from your own GPU box
|
||||
- You are wiring LM Studio or an OpenAI-compatible proxy
|
||||
- You need the safest local model guidance
|
||||
title: "Local Models"
|
||||
---
|
||||
|
||||
# Local models
|
||||
|
||||
Local is doable, but OpenClaw expects large context + strong defenses against prompt injection. Small cards truncate context and leak safety. Aim high: **≥2 maxed-out Mac Studios or equivalent GPU rig (~$30k+)**. A single **24 GB** GPU works only for lighter prompts with higher latency. Use the **largest / full-size model variant you can run**; aggressively quantized or “small” checkpoints raise prompt-injection risk (see [Security](/gateway/security)).
|
||||
|
||||
If you want the lowest-friction local setup, start with [LM Studio](/providers/lmstudio) or [Ollama](/providers/ollama) and `openclaw onboard`. This page is the opinionated guide for higher-end local stacks and custom OpenAI-compatible local servers.
|
||||
|
||||
## Recommended: LM Studio + large local model (Responses API)
|
||||
|
||||
Best current local stack. Load a large model in LM Studio (for example, a full-size Qwen, DeepSeek, or Llama build), enable the local server (default `http://127.0.0.1:1234`), and use Responses API to keep reasoning separate from final text.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: “lmstudio/my-local-model” },
|
||||
models: {
|
||||
“anthropic/claude-opus-4-6”: { alias: “Opus” },
|
||||
“lmstudio/my-local-model”: { alias: “Local” },
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: “merge”,
|
||||
providers: {
|
||||
lmstudio: {
|
||||
baseUrl: “http://127.0.0.1:1234/v1”,
|
||||
apiKey: “lmstudio”,
|
||||
api: “openai-responses”,
|
||||
models: [
|
||||
{
|
||||
id: “my-local-model”,
|
||||
name: “Local Model”,
|
||||
reasoning: false,
|
||||
input: [“text”],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 196608,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Setup checklist**
|
||||
|
||||
- Install LM Studio: [https://lmstudio.ai](https://lmstudio.ai)
|
||||
- In LM Studio, download the **largest model build available** (avoid “small”/heavily quantized variants), start the server, confirm `http://127.0.0.1:1234/v1/models` lists it.
|
||||
- Replace `my-local-model` with the actual model ID shown in LM Studio.
|
||||
- Keep the model loaded; cold-load adds startup latency.
|
||||
- Adjust `contextWindow`/`maxTokens` if your LM Studio build differs.
|
||||
- For WhatsApp, stick to Responses API so only final text is sent.
|
||||
|
||||
Keep hosted models configured even when running local; use `models.mode: "merge"` so fallbacks stay available.
|
||||
|
||||
### Hybrid config: hosted primary, local fallback
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4-6",
|
||||
fallbacks: ["lmstudio/my-local-model", "anthropic/claude-opus-4-6"],
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||
"lmstudio/my-local-model": { alias: "Local" },
|
||||
"anthropic/claude-opus-4-6": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: "merge",
|
||||
providers: {
|
||||
lmstudio: {
|
||||
baseUrl: "http://127.0.0.1:1234/v1",
|
||||
apiKey: "lmstudio",
|
||||
api: "openai-responses",
|
||||
models: [
|
||||
{
|
||||
id: "my-local-model",
|
||||
name: "Local Model",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 196608,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Local-first with hosted safety net
|
||||
|
||||
Swap the primary and fallback order; keep the same providers block and `models.mode: "merge"` so you can fall back to Sonnet or Opus when the local box is down.
|
||||
|
||||
### Regional hosting / data routing
|
||||
|
||||
- Hosted MiniMax/Kimi/GLM variants also exist on OpenRouter with region-pinned endpoints (e.g., US-hosted). Pick the regional variant there to keep traffic in your chosen jurisdiction while still using `models.mode: "merge"` for Anthropic/OpenAI fallbacks.
|
||||
- Local-only remains the strongest privacy path; hosted regional routing is the middle ground when you need provider features but want control over data flow.
|
||||
|
||||
## Other OpenAI-compatible local proxies
|
||||
|
||||
vLLM, LiteLLM, OAI-proxy, or custom gateways work if they expose an OpenAI-style `/v1` endpoint. Replace the provider block above with your endpoint and model ID:
|
||||
|
||||
```json5
|
||||
{
|
||||
models: {
|
||||
mode: "merge",
|
||||
providers: {
|
||||
local: {
|
||||
baseUrl: "http://127.0.0.1:8000/v1",
|
||||
apiKey: "sk-local",
|
||||
api: "openai-responses",
|
||||
models: [
|
||||
{
|
||||
id: "my-local-model",
|
||||
name: "Local Model",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 120000,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Keep `models.mode: "merge"` so hosted models stay available as fallbacks.
|
||||
|
||||
Behavior note for local/proxied `/v1` backends:
|
||||
|
||||
- OpenClaw treats these as proxy-style OpenAI-compatible routes, not native
|
||||
OpenAI endpoints
|
||||
- native OpenAI-only request shaping does not apply here: no
|
||||
`service_tier`, no Responses `store`, no OpenAI reasoning-compat payload
|
||||
shaping, and no prompt-cache hints
|
||||
- hidden OpenClaw attribution headers (`originator`, `version`, `User-Agent`)
|
||||
are not injected on these custom proxy URLs
|
||||
|
||||
Compatibility notes for stricter OpenAI-compatible backends:
|
||||
|
||||
- Some servers accept only string `messages[].content` on Chat Completions, not
|
||||
structured content-part arrays. Set
|
||||
`models.providers.<provider>.models[].compat.requiresStringContent: true` for
|
||||
those endpoints.
|
||||
- Some smaller or stricter local backends are unstable with OpenClaw's full
|
||||
agent-runtime prompt shape, especially when tool schemas are included. If the
|
||||
backend works for tiny direct `/v1/chat/completions` calls but fails on normal
|
||||
OpenClaw agent turns, first try
|
||||
`agents.defaults.experimental.localModelLean: true` to drop heavyweight
|
||||
default tools like `browser`, `cron`, and `message`; this is an experimental
|
||||
flag, not a stable default-mode setting. See
|
||||
[Experimental Features](/concepts/experimental-features). If that still fails, try
|
||||
`models.providers.<provider>.models[].compat.supportsTools: false`.
|
||||
- If the backend still fails only on larger OpenClaw runs, the remaining issue
|
||||
is usually upstream model/server capacity or a backend bug, not OpenClaw's
|
||||
transport layer.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Gateway can reach the proxy? `curl http://127.0.0.1:1234/v1/models`.
|
||||
- LM Studio model unloaded? Reload; cold start is a common “hanging” cause.
|
||||
- OpenClaw warns when the detected context window is below **32k** and blocks below **16k**. If you hit that preflight, raise the server/model context limit or choose a larger model.
|
||||
- Context errors? Lower `contextWindow` or raise your server limit.
|
||||
- OpenAI-compatible server returns `messages[].content ... expected a string`?
|
||||
Add `compat.requiresStringContent: true` on that model entry.
|
||||
- Direct tiny `/v1/chat/completions` calls work, but `openclaw infer model run`
|
||||
fails on Gemma or another local model? Disable tool schemas first with
|
||||
`compat.supportsTools: false`, then retest. If the server still crashes only
|
||||
on larger OpenClaw prompts, treat it as an upstream server/model limitation.
|
||||
- Safety: local models skip provider-side filters; keep agents narrow and compaction on to limit prompt injection blast radius.
|
||||
113
openclaw/docs/gateway/logging.md
Normal file
113
openclaw/docs/gateway/logging.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
summary: "Logging surfaces, file logs, WS log styles, and console formatting"
|
||||
read_when:
|
||||
- Changing logging output or formats
|
||||
- Debugging CLI or gateway output
|
||||
title: "Gateway Logging"
|
||||
---
|
||||
|
||||
# Logging
|
||||
|
||||
For a user-facing overview (CLI + Control UI + config), see [/logging](/logging).
|
||||
|
||||
OpenClaw has two log “surfaces”:
|
||||
|
||||
- **Console output** (what you see in the terminal / Debug UI).
|
||||
- **File logs** (JSON lines) written by the gateway logger.
|
||||
|
||||
## File-based logger
|
||||
|
||||
- Default rolling log file is under `/tmp/openclaw/` (one file per day): `openclaw-YYYY-MM-DD.log`
|
||||
- Date uses the gateway host's local timezone.
|
||||
- The log file path and level can be configured via `~/.openclaw/openclaw.json`:
|
||||
- `logging.file`
|
||||
- `logging.level`
|
||||
|
||||
The file format is one JSON object per line.
|
||||
|
||||
The Control UI Logs tab tails this file via the gateway (`logs.tail`).
|
||||
CLI can do the same:
|
||||
|
||||
```bash
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
**Verbose vs. log levels**
|
||||
|
||||
- **File logs** are controlled exclusively by `logging.level`.
|
||||
- `--verbose` only affects **console verbosity** (and WS log style); it does **not**
|
||||
raise the file log level.
|
||||
- To capture verbose-only details in file logs, set `logging.level` to `debug` or
|
||||
`trace`.
|
||||
|
||||
## Console capture
|
||||
|
||||
The CLI captures `console.log/info/warn/error/debug/trace` and writes them to file logs,
|
||||
while still printing to stdout/stderr.
|
||||
|
||||
You can tune console verbosity independently via:
|
||||
|
||||
- `logging.consoleLevel` (default `info`)
|
||||
- `logging.consoleStyle` (`pretty` | `compact` | `json`)
|
||||
|
||||
## Tool summary redaction
|
||||
|
||||
Verbose tool summaries (e.g. `🛠️ Exec: ...`) can mask sensitive tokens before they hit the
|
||||
console stream. This is **tools-only** and does not alter file logs.
|
||||
|
||||
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)
|
||||
- `logging.redactPatterns`: array of regex strings (overrides defaults)
|
||||
- Use raw regex strings (auto `gi`), or `/pattern/flags` if you need custom flags.
|
||||
- Matches are masked by keeping the first 6 + last 4 chars (length >= 18), otherwise `***`.
|
||||
- Defaults cover common key assignments, CLI flags, JSON fields, bearer headers, PEM blocks, and popular token prefixes.
|
||||
|
||||
## Gateway WebSocket logs
|
||||
|
||||
The gateway prints WebSocket protocol logs in two modes:
|
||||
|
||||
- **Normal mode (no `--verbose`)**: only “interesting” RPC results are printed:
|
||||
- errors (`ok=false`)
|
||||
- slow calls (default threshold: `>= 50ms`)
|
||||
- parse errors
|
||||
- **Verbose mode (`--verbose`)**: prints all WS request/response traffic.
|
||||
|
||||
### WS log style
|
||||
|
||||
`openclaw gateway` supports a per-gateway style switch:
|
||||
|
||||
- `--ws-log auto` (default): normal mode is optimized; verbose mode uses compact output
|
||||
- `--ws-log compact`: compact output (paired request/response) when verbose
|
||||
- `--ws-log full`: full per-frame output when verbose
|
||||
- `--compact`: alias for `--ws-log compact`
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# optimized (only errors/slow)
|
||||
openclaw gateway
|
||||
|
||||
# show all WS traffic (paired)
|
||||
openclaw gateway --verbose --ws-log compact
|
||||
|
||||
# show all WS traffic (full meta)
|
||||
openclaw gateway --verbose --ws-log full
|
||||
```
|
||||
|
||||
## Console formatting (subsystem logging)
|
||||
|
||||
The console formatter is **TTY-aware** and prints consistent, prefixed lines.
|
||||
Subsystem loggers keep output grouped and scannable.
|
||||
|
||||
Behavior:
|
||||
|
||||
- **Subsystem prefixes** on every line (e.g. `[gateway]`, `[canvas]`, `[tailscale]`)
|
||||
- **Subsystem colors** (stable per subsystem) plus level coloring
|
||||
- **Color when output is a TTY or the environment looks like a rich terminal** (`TERM`/`COLORTERM`/`TERM_PROGRAM`), respects `NO_COLOR`
|
||||
- **Shortened subsystem prefixes**: drops leading `gateway/` + `channels/`, keeps last 2 segments (e.g. `whatsapp/outbound`)
|
||||
- **Sub-loggers by subsystem** (auto prefix + structured field `{ subsystem }`)
|
||||
- **`logRaw()`** for QR/UX output (no prefix, no formatting)
|
||||
- **Console styles** (e.g. `pretty | compact | json`)
|
||||
- **Console log level** separate from file log level (file keeps full detail when `logging.level` is set to `debug`/`trace`)
|
||||
- **WhatsApp message bodies** are logged at `debug` (use `--verbose` to see them)
|
||||
|
||||
This keeps existing file logs stable while making interactive output scannable.
|
||||
120
openclaw/docs/gateway/multiple-gateways.md
Normal file
120
openclaw/docs/gateway/multiple-gateways.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
---
|
||||
summary: "Run multiple OpenClaw Gateways on one host (isolation, ports, and profiles)"
|
||||
read_when:
|
||||
- Running more than one Gateway on the same machine
|
||||
- You need isolated config/state/ports per Gateway
|
||||
title: "Multiple Gateways"
|
||||
---
|
||||
|
||||
# Multiple Gateways (same host)
|
||||
|
||||
Most setups should use one Gateway because a single Gateway can handle multiple messaging connections and agents. If you need stronger isolation or redundancy (e.g., a rescue bot), run separate Gateways with isolated profiles/ports.
|
||||
|
||||
## Isolation checklist (required)
|
||||
|
||||
- `OPENCLAW_CONFIG_PATH` — per-instance config file
|
||||
- `OPENCLAW_STATE_DIR` — per-instance sessions, creds, caches
|
||||
- `agents.defaults.workspace` — per-instance workspace root
|
||||
- `gateway.port` (or `--port`) — unique per instance
|
||||
- Derived ports (browser/canvas) must not overlap
|
||||
|
||||
If these are shared, you will hit config races and port conflicts.
|
||||
|
||||
## Recommended: profiles (`--profile`)
|
||||
|
||||
Profiles auto-scope `OPENCLAW_STATE_DIR` + `OPENCLAW_CONFIG_PATH` and suffix service names.
|
||||
|
||||
```bash
|
||||
# main
|
||||
openclaw --profile main setup
|
||||
openclaw --profile main gateway --port 18789
|
||||
|
||||
# rescue
|
||||
openclaw --profile rescue setup
|
||||
openclaw --profile rescue gateway --port 19001
|
||||
```
|
||||
|
||||
Per-profile services:
|
||||
|
||||
```bash
|
||||
openclaw --profile main gateway install
|
||||
openclaw --profile rescue gateway install
|
||||
```
|
||||
|
||||
## Rescue-bot guide
|
||||
|
||||
Run a second Gateway on the same host with its own:
|
||||
|
||||
- profile/config
|
||||
- state dir
|
||||
- workspace
|
||||
- base port (plus derived ports)
|
||||
|
||||
This keeps the rescue bot isolated from the main bot so it can debug or apply config changes if the primary bot is down.
|
||||
|
||||
Port spacing: leave at least 20 ports between base ports so the derived browser/canvas/CDP ports never collide.
|
||||
|
||||
### How to install (rescue bot)
|
||||
|
||||
```bash
|
||||
# Main bot (existing or fresh, without --profile param)
|
||||
# Runs on port 18789 + Chrome CDC/Canvas/... Ports
|
||||
openclaw onboard
|
||||
openclaw gateway install
|
||||
|
||||
# Rescue bot (isolated profile + ports)
|
||||
openclaw --profile rescue onboard
|
||||
# Notes:
|
||||
# - workspace name will be postfixed with -rescue per default
|
||||
# - Port should be at least 18789 + 20 Ports,
|
||||
# better choose completely different base port, like 19789,
|
||||
# - rest of the onboarding is the same as normal
|
||||
|
||||
# To install the service (if not happened automatically during setup)
|
||||
openclaw --profile rescue gateway install
|
||||
```
|
||||
|
||||
## Port mapping (derived)
|
||||
|
||||
Base port = `gateway.port` (or `OPENCLAW_GATEWAY_PORT` / `--port`).
|
||||
|
||||
- browser control service port = base + 2 (loopback only)
|
||||
- canvas host is served on the Gateway HTTP server (same port as `gateway.port`)
|
||||
- Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108`
|
||||
|
||||
If you override any of these in config or env, you must keep them unique per instance.
|
||||
|
||||
## Browser/CDP notes (common footgun)
|
||||
|
||||
- Do **not** pin `browser.cdpUrl` to the same values on multiple instances.
|
||||
- Each instance needs its own browser control port and CDP range (derived from its gateway port).
|
||||
- If you need explicit CDP ports, set `browser.profiles.<name>.cdpPort` per instance.
|
||||
- Remote Chrome: use `browser.profiles.<name>.cdpUrl` (per profile, per instance).
|
||||
|
||||
## Manual env example
|
||||
|
||||
```bash
|
||||
OPENCLAW_CONFIG_PATH=~/.openclaw/main.json \
|
||||
OPENCLAW_STATE_DIR=~/.openclaw-main \
|
||||
openclaw gateway --port 18789
|
||||
|
||||
OPENCLAW_CONFIG_PATH=~/.openclaw/rescue.json \
|
||||
OPENCLAW_STATE_DIR=~/.openclaw-rescue \
|
||||
openclaw gateway --port 19001
|
||||
```
|
||||
|
||||
## Quick checks
|
||||
|
||||
```bash
|
||||
openclaw --profile main gateway status --deep
|
||||
openclaw --profile rescue gateway status --deep
|
||||
openclaw --profile rescue gateway probe
|
||||
openclaw --profile main status
|
||||
openclaw --profile rescue status
|
||||
openclaw --profile rescue browser status
|
||||
```
|
||||
|
||||
Interpretation:
|
||||
|
||||
- `gateway status --deep` helps catch stale launchd/systemd/schtasks services from older installs.
|
||||
- `gateway probe` warning text such as `multiple reachable gateways detected` is expected only when you intentionally run more than one isolated gateway.
|
||||
25
openclaw/docs/gateway/network-model.md
Normal file
25
openclaw/docs/gateway/network-model.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
summary: "How the Gateway, nodes, and canvas host connect."
|
||||
read_when:
|
||||
- You want a concise view of the Gateway networking model
|
||||
title: "Network model"
|
||||
---
|
||||
|
||||
# Network Model
|
||||
|
||||
> This content has been merged into [Network](/network#core-model). See that page for the current guide.
|
||||
|
||||
Most operations flow through the Gateway (`openclaw gateway`), a single long-running
|
||||
process that owns channel connections and the WebSocket control plane.
|
||||
|
||||
## Core rules
|
||||
|
||||
- One Gateway per host is recommended. It is the only process allowed to own the WhatsApp Web session. For rescue bots or strict isolation, run multiple gateways with isolated profiles and ports. See [Multiple gateways](/gateway/multiple-gateways).
|
||||
- Loopback first: the Gateway WS defaults to `ws://127.0.0.1:18789`. The wizard creates shared-secret auth by default and usually generates a token, even for loopback. For non-loopback access, use a valid gateway auth path: shared-secret token/password auth, or a correctly configured non-loopback `trusted-proxy` deployment. Tailnet/mobile setups usually work best through Tailscale Serve or another `wss://` endpoint instead of raw tailnet `ws://`.
|
||||
- Nodes connect to the Gateway WS over LAN, tailnet, or SSH as needed. The
|
||||
legacy TCP bridge has been removed.
|
||||
- Canvas host is served by the Gateway HTTP server on the **same port** as the Gateway (default `18789`):
|
||||
- `/__openclaw__/canvas/`
|
||||
- `/__openclaw__/a2ui/`
|
||||
When `gateway.auth` is configured and the Gateway binds beyond loopback, these routes are protected by Gateway auth. Node clients use node-scoped capability URLs tied to their active WS session. See [Gateway configuration](/gateway/configuration) (`canvasHost`, `gateway`).
|
||||
- Remote use is typically SSH tunnel or tailnet VPN. See [Remote access](/gateway/remote) and [Discovery](/gateway/discovery).
|
||||
280
openclaw/docs/gateway/openai-http-api.md
Normal file
280
openclaw/docs/gateway/openai-http-api.md
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
---
|
||||
summary: "Expose an OpenAI-compatible /v1/chat/completions HTTP endpoint from the Gateway"
|
||||
read_when:
|
||||
- Integrating tools that expect OpenAI Chat Completions
|
||||
title: "OpenAI Chat Completions"
|
||||
---
|
||||
|
||||
# OpenAI Chat Completions (HTTP)
|
||||
|
||||
OpenClaw’s Gateway can serve a small OpenAI-compatible Chat Completions endpoint.
|
||||
|
||||
This endpoint is **disabled by default**. Enable it in config first.
|
||||
|
||||
- `POST /v1/chat/completions`
|
||||
- Same port as the Gateway (WS + HTTP multiplex): `http://<gateway-host>:<port>/v1/chat/completions`
|
||||
|
||||
When the Gateway’s OpenAI-compatible HTTP surface is enabled, it also serves:
|
||||
|
||||
- `GET /v1/models`
|
||||
- `GET /v1/models/{id}`
|
||||
- `POST /v1/embeddings`
|
||||
- `POST /v1/responses`
|
||||
|
||||
Under the hood, requests are executed as a normal Gateway agent run (same codepath as `openclaw agent`), so routing/permissions/config match your Gateway.
|
||||
|
||||
## Authentication
|
||||
|
||||
Uses the Gateway auth configuration.
|
||||
|
||||
Common HTTP auth paths:
|
||||
|
||||
- shared-secret auth (`gateway.auth.mode="token"` or `"password"`):
|
||||
`Authorization: Bearer <token-or-password>`
|
||||
- trusted identity-bearing HTTP auth (`gateway.auth.mode="trusted-proxy"`):
|
||||
route through the configured identity-aware proxy and let it inject the
|
||||
required identity headers
|
||||
- private-ingress open auth (`gateway.auth.mode="none"`):
|
||||
no auth header required
|
||||
|
||||
Notes:
|
||||
|
||||
- When `gateway.auth.mode="token"`, use `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`).
|
||||
- When `gateway.auth.mode="password"`, use `gateway.auth.password` (or `OPENCLAW_GATEWAY_PASSWORD`).
|
||||
- When `gateway.auth.mode="trusted-proxy"`, the HTTP request must come from a
|
||||
configured non-loopback trusted proxy source; same-host loopback proxies do
|
||||
not satisfy this mode.
|
||||
- If `gateway.auth.rateLimit` is configured and too many auth failures occur, the endpoint returns `429` with `Retry-After`.
|
||||
|
||||
## Security boundary (important)
|
||||
|
||||
Treat this endpoint as a **full operator-access** surface for the gateway instance.
|
||||
|
||||
- HTTP bearer auth here is not a narrow per-user scope model.
|
||||
- A valid Gateway token/password for this endpoint should be treated like an owner/operator credential.
|
||||
- Requests run through the same control-plane agent path as trusted operator actions.
|
||||
- There is no separate non-owner/per-user tool boundary on this endpoint; once a caller passes Gateway auth here, OpenClaw treats that caller as a trusted operator for this gateway.
|
||||
- For shared-secret auth modes (`token` and `password`), the endpoint restores the normal full operator defaults even if the caller sends a narrower `x-openclaw-scopes` header.
|
||||
- Trusted identity-bearing HTTP modes (for example trusted proxy auth or `gateway.auth.mode="none"`) honor `x-openclaw-scopes` when present and otherwise fall back to the normal operator default scope set.
|
||||
- If the target agent policy allows sensitive tools, this endpoint can use them.
|
||||
- Keep this endpoint on loopback/tailnet/private ingress only; do not expose it directly to the public internet.
|
||||
|
||||
Auth matrix:
|
||||
|
||||
- `gateway.auth.mode="token"` or `"password"` + `Authorization: Bearer ...`
|
||||
- proves possession of the shared gateway operator secret
|
||||
- ignores narrower `x-openclaw-scopes`
|
||||
- restores the full default operator scope set:
|
||||
`operator.admin`, `operator.approvals`, `operator.pairing`,
|
||||
`operator.read`, `operator.talk.secrets`, `operator.write`
|
||||
- treats chat turns on this endpoint as owner-sender turns
|
||||
- trusted identity-bearing HTTP modes (for example trusted proxy auth, or `gateway.auth.mode="none"` on private ingress)
|
||||
- authenticate some outer trusted identity or deployment boundary
|
||||
- honor `x-openclaw-scopes` when the header is present
|
||||
- fall back to the normal operator default scope set when the header is absent
|
||||
- only lose owner semantics when the caller explicitly narrows scopes and omits `operator.admin`
|
||||
|
||||
See [Security](/gateway/security) and [Remote access](/gateway/remote).
|
||||
|
||||
## Agent-first model contract
|
||||
|
||||
OpenClaw treats the OpenAI `model` field as an **agent target**, not a raw provider model id.
|
||||
|
||||
- `model: "openclaw"` routes to the configured default agent.
|
||||
- `model: "openclaw/default"` also routes to the configured default agent.
|
||||
- `model: "openclaw/<agentId>"` routes to a specific agent.
|
||||
|
||||
Optional request headers:
|
||||
|
||||
- `x-openclaw-model: <provider/model-or-bare-id>` overrides the backend model for the selected agent.
|
||||
- `x-openclaw-agent-id: <agentId>` remains supported as a compatibility override.
|
||||
- `x-openclaw-session-key: <sessionKey>` fully controls session routing.
|
||||
- `x-openclaw-message-channel: <channel>` sets the synthetic ingress channel context for channel-aware prompts and policies.
|
||||
|
||||
Compatibility aliases still accepted:
|
||||
|
||||
- `model: "openclaw:<agentId>"`
|
||||
- `model: "agent:<agentId>"`
|
||||
|
||||
## Enabling the endpoint
|
||||
|
||||
Set `gateway.http.endpoints.chatCompletions.enabled` to `true`:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
http: {
|
||||
endpoints: {
|
||||
chatCompletions: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Disabling the endpoint
|
||||
|
||||
Set `gateway.http.endpoints.chatCompletions.enabled` to `false`:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
http: {
|
||||
endpoints: {
|
||||
chatCompletions: { enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Session behavior
|
||||
|
||||
By default the endpoint is **stateless per request** (a new session key is generated each call).
|
||||
|
||||
If the request includes an OpenAI `user` string, the Gateway derives a stable session key from it, so repeated calls can share an agent session.
|
||||
|
||||
## Why this surface matters
|
||||
|
||||
This is the highest-leverage compatibility set for self-hosted frontends and tooling:
|
||||
|
||||
- Most Open WebUI, LobeChat, and LibreChat setups expect `/v1/models`.
|
||||
- Many RAG systems expect `/v1/embeddings`.
|
||||
- Existing OpenAI chat clients can usually start with `/v1/chat/completions`.
|
||||
- More agent-native clients increasingly prefer `/v1/responses`.
|
||||
|
||||
## Model list and agent routing
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="What does `/v1/models` return?">
|
||||
An OpenClaw agent-target list.
|
||||
|
||||
The returned ids are `openclaw`, `openclaw/default`, and `openclaw/<agentId>` entries.
|
||||
Use them directly as OpenAI `model` values.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Does `/v1/models` list agents or sub-agents?">
|
||||
It lists top-level agent targets, not backend provider models and not sub-agents.
|
||||
|
||||
Sub-agents remain internal execution topology. They do not appear as pseudo-models.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Why is `openclaw/default` included?">
|
||||
`openclaw/default` is the stable alias for the configured default agent.
|
||||
|
||||
That means clients can keep using one predictable id even if the real default agent id changes between environments.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="How do I override the backend model?">
|
||||
Use `x-openclaw-model`.
|
||||
|
||||
Examples:
|
||||
`x-openclaw-model: openai/gpt-5.4`
|
||||
`x-openclaw-model: gpt-5.4`
|
||||
|
||||
If you omit it, the selected agent runs with its normal configured model choice.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="How do embeddings fit this contract?">
|
||||
`/v1/embeddings` uses the same agent-target `model` ids.
|
||||
|
||||
Use `model: "openclaw/default"` or `model: "openclaw/<agentId>"`.
|
||||
When you need a specific embedding model, send it in `x-openclaw-model`.
|
||||
Without that header, the request passes through to the selected agent's normal embedding setup.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Streaming (SSE)
|
||||
|
||||
Set `stream: true` to receive Server-Sent Events (SSE):
|
||||
|
||||
- `Content-Type: text/event-stream`
|
||||
- Each event line is `data: <json>`
|
||||
- Stream ends with `data: [DONE]`
|
||||
|
||||
## Open WebUI quick setup
|
||||
|
||||
For a basic Open WebUI connection:
|
||||
|
||||
- Base URL: `http://127.0.0.1:18789/v1`
|
||||
- Docker on macOS base URL: `http://host.docker.internal:18789/v1`
|
||||
- API key: your Gateway bearer token
|
||||
- Model: `openclaw/default`
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- `GET /v1/models` should list `openclaw/default`
|
||||
- Open WebUI should use `openclaw/default` as the chat model id
|
||||
- If you want a specific backend provider/model for that agent, set the agent's normal default model or send `x-openclaw-model`
|
||||
|
||||
Quick smoke:
|
||||
|
||||
```bash
|
||||
curl -sS http://127.0.0.1:18789/v1/models \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN'
|
||||
```
|
||||
|
||||
If that returns `openclaw/default`, most Open WebUI setups can connect with the same base URL and token.
|
||||
|
||||
## Examples
|
||||
|
||||
Non-streaming:
|
||||
|
||||
```bash
|
||||
curl -sS http://127.0.0.1:18789/v1/chat/completions \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"model": "openclaw/default",
|
||||
"messages": [{"role":"user","content":"hi"}]
|
||||
}'
|
||||
```
|
||||
|
||||
Streaming:
|
||||
|
||||
```bash
|
||||
curl -N http://127.0.0.1:18789/v1/chat/completions \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'x-openclaw-model: openai/gpt-5.4' \
|
||||
-d '{
|
||||
"model": "openclaw/research",
|
||||
"stream": true,
|
||||
"messages": [{"role":"user","content":"hi"}]
|
||||
}'
|
||||
```
|
||||
|
||||
List models:
|
||||
|
||||
```bash
|
||||
curl -sS http://127.0.0.1:18789/v1/models \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN'
|
||||
```
|
||||
|
||||
Fetch one model:
|
||||
|
||||
```bash
|
||||
curl -sS http://127.0.0.1:18789/v1/models/openclaw%2Fdefault \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN'
|
||||
```
|
||||
|
||||
Create embeddings:
|
||||
|
||||
```bash
|
||||
curl -sS http://127.0.0.1:18789/v1/embeddings \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'x-openclaw-model: openai/text-embedding-3-small' \
|
||||
-d '{
|
||||
"model": "openclaw/default",
|
||||
"input": ["alpha", "beta"]
|
||||
}'
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `/v1/models` returns OpenClaw agent targets, not raw provider catalogs.
|
||||
- `openclaw/default` is always present so one stable id works across environments.
|
||||
- Backend provider/model overrides belong in `x-openclaw-model`, not the OpenAI `model` field.
|
||||
- `/v1/embeddings` supports `input` as a string or array of strings.
|
||||
340
openclaw/docs/gateway/openresponses-http-api.md
Normal file
340
openclaw/docs/gateway/openresponses-http-api.md
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
---
|
||||
summary: "Expose an OpenResponses-compatible /v1/responses HTTP endpoint from the Gateway"
|
||||
read_when:
|
||||
- Integrating clients that speak the OpenResponses API
|
||||
- You want item-based inputs, client tool calls, or SSE events
|
||||
title: "OpenResponses API"
|
||||
---
|
||||
|
||||
# OpenResponses API (HTTP)
|
||||
|
||||
OpenClaw’s Gateway can serve an OpenResponses-compatible `POST /v1/responses` endpoint.
|
||||
|
||||
This endpoint is **disabled by default**. Enable it in config first.
|
||||
|
||||
- `POST /v1/responses`
|
||||
- Same port as the Gateway (WS + HTTP multiplex): `http://<gateway-host>:<port>/v1/responses`
|
||||
|
||||
Under the hood, requests are executed as a normal Gateway agent run (same codepath as
|
||||
`openclaw agent`), so routing/permissions/config match your Gateway.
|
||||
|
||||
## Authentication, security, and routing
|
||||
|
||||
Operational behavior matches [OpenAI Chat Completions](/gateway/openai-http-api):
|
||||
|
||||
- use the matching Gateway HTTP auth path:
|
||||
- shared-secret auth (`gateway.auth.mode="token"` or `"password"`): `Authorization: Bearer <token-or-password>`
|
||||
- trusted-proxy auth (`gateway.auth.mode="trusted-proxy"`): identity-aware proxy headers from a configured non-loopback trusted proxy source
|
||||
- private-ingress open auth (`gateway.auth.mode="none"`): no auth header
|
||||
- treat the endpoint as full operator access for the gateway instance
|
||||
- for shared-secret auth modes (`token` and `password`), ignore narrower bearer-declared `x-openclaw-scopes` values and restore the normal full operator defaults
|
||||
- for trusted identity-bearing HTTP modes (for example trusted proxy auth or `gateway.auth.mode="none"`), honor `x-openclaw-scopes` when present and otherwise fall back to the normal operator default scope set
|
||||
- select agents with `model: "openclaw"`, `model: "openclaw/default"`, `model: "openclaw/<agentId>"`, or `x-openclaw-agent-id`
|
||||
- use `x-openclaw-model` when you want to override the selected agent's backend model
|
||||
- use `x-openclaw-session-key` for explicit session routing
|
||||
- use `x-openclaw-message-channel` when you want a non-default synthetic ingress channel context
|
||||
|
||||
Auth matrix:
|
||||
|
||||
- `gateway.auth.mode="token"` or `"password"` + `Authorization: Bearer ...`
|
||||
- proves possession of the shared gateway operator secret
|
||||
- ignores narrower `x-openclaw-scopes`
|
||||
- restores the full default operator scope set:
|
||||
`operator.admin`, `operator.approvals`, `operator.pairing`,
|
||||
`operator.read`, `operator.talk.secrets`, `operator.write`
|
||||
- treats chat turns on this endpoint as owner-sender turns
|
||||
- trusted identity-bearing HTTP modes (for example trusted proxy auth, or `gateway.auth.mode="none"` on private ingress)
|
||||
- honor `x-openclaw-scopes` when the header is present
|
||||
- fall back to the normal operator default scope set when the header is absent
|
||||
- only lose owner semantics when the caller explicitly narrows scopes and omits `operator.admin`
|
||||
|
||||
Enable or disable this endpoint with `gateway.http.endpoints.responses.enabled`.
|
||||
|
||||
The same compatibility surface also includes:
|
||||
|
||||
- `GET /v1/models`
|
||||
- `GET /v1/models/{id}`
|
||||
- `POST /v1/embeddings`
|
||||
- `POST /v1/chat/completions`
|
||||
|
||||
For the canonical explanation of how agent-target models, `openclaw/default`, embeddings pass-through, and backend model overrides fit together, see [OpenAI Chat Completions](/gateway/openai-http-api#agent-first-model-contract) and [Model list and agent routing](/gateway/openai-http-api#model-list-and-agent-routing).
|
||||
|
||||
## Session behavior
|
||||
|
||||
By default the endpoint is **stateless per request** (a new session key is generated each call).
|
||||
|
||||
If the request includes an OpenResponses `user` string, the Gateway derives a stable session key
|
||||
from it, so repeated calls can share an agent session.
|
||||
|
||||
## Request shape (supported)
|
||||
|
||||
The request follows the OpenResponses API with item-based input. Current support:
|
||||
|
||||
- `input`: string or array of item objects.
|
||||
- `instructions`: merged into the system prompt.
|
||||
- `tools`: client tool definitions (function tools).
|
||||
- `tool_choice`: filter or require client tools.
|
||||
- `stream`: enables SSE streaming.
|
||||
- `max_output_tokens`: best-effort output limit (provider dependent).
|
||||
- `user`: stable session routing.
|
||||
|
||||
Accepted but **currently ignored**:
|
||||
|
||||
- `max_tool_calls`
|
||||
- `reasoning`
|
||||
- `metadata`
|
||||
- `store`
|
||||
- `truncation`
|
||||
|
||||
Supported:
|
||||
|
||||
- `previous_response_id`: OpenClaw reuses the earlier response session when the request stays within the same agent/user/requested-session scope.
|
||||
|
||||
## Items (input)
|
||||
|
||||
### `message`
|
||||
|
||||
Roles: `system`, `developer`, `user`, `assistant`.
|
||||
|
||||
- `system` and `developer` are appended to the system prompt.
|
||||
- The most recent `user` or `function_call_output` item becomes the “current message.”
|
||||
- Earlier user/assistant messages are included as history for context.
|
||||
|
||||
### `function_call_output` (turn-based tools)
|
||||
|
||||
Send tool results back to the model:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_123",
|
||||
"output": "{\"temperature\": \"72F\"}"
|
||||
}
|
||||
```
|
||||
|
||||
### `reasoning` and `item_reference`
|
||||
|
||||
Accepted for schema compatibility but ignored when building the prompt.
|
||||
|
||||
## Tools (client-side function tools)
|
||||
|
||||
Provide tools with `tools: [{ type: "function", function: { name, description?, parameters? } }]`.
|
||||
|
||||
If the agent decides to call a tool, the response returns a `function_call` output item.
|
||||
You then send a follow-up request with `function_call_output` to continue the turn.
|
||||
|
||||
## Images (`input_image`)
|
||||
|
||||
Supports base64 or URL sources:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "input_image",
|
||||
"source": { "type": "url", "url": "https://example.com/image.png" }
|
||||
}
|
||||
```
|
||||
|
||||
Allowed MIME types (current): `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/heic`, `image/heif`.
|
||||
Max size (current): 10MB.
|
||||
|
||||
## Files (`input_file`)
|
||||
|
||||
Supports base64 or URL sources:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "input_file",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "text/plain",
|
||||
"data": "SGVsbG8gV29ybGQh",
|
||||
"filename": "hello.txt"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Allowed MIME types (current): `text/plain`, `text/markdown`, `text/html`, `text/csv`,
|
||||
`application/json`, `application/pdf`.
|
||||
|
||||
Max size (current): 5MB.
|
||||
|
||||
Current behavior:
|
||||
|
||||
- File content is decoded and added to the **system prompt**, not the user message,
|
||||
so it stays ephemeral (not persisted in session history).
|
||||
- Decoded file text is wrapped as **untrusted external content** before it is added,
|
||||
so file bytes are treated as data, not trusted instructions.
|
||||
- The injected block uses explicit boundary markers like
|
||||
`<<<EXTERNAL_UNTRUSTED_CONTENT id="...">>>` /
|
||||
`<<<END_EXTERNAL_UNTRUSTED_CONTENT id="...">>>` and includes a
|
||||
`Source: External` metadata line.
|
||||
- This file-input path intentionally omits the long `SECURITY NOTICE:` banner to
|
||||
preserve prompt budget; the boundary markers and metadata still stay in place.
|
||||
- PDFs are parsed for text first. If little text is found, the first pages are
|
||||
rasterized into images and passed to the model, and the injected file block uses
|
||||
the placeholder `[PDF content rendered to images]`.
|
||||
|
||||
PDF parsing uses the Node-friendly `pdfjs-dist` legacy build (no worker). The modern
|
||||
PDF.js build expects browser workers/DOM globals, so it is not used in the Gateway.
|
||||
|
||||
URL fetch defaults:
|
||||
|
||||
- `files.allowUrl`: `true`
|
||||
- `images.allowUrl`: `true`
|
||||
- `maxUrlParts`: `8` (total URL-based `input_file` + `input_image` parts per request)
|
||||
- Requests are guarded (DNS resolution, private IP blocking, redirect caps, timeouts).
|
||||
- Optional hostname allowlists are supported per input type (`files.urlAllowlist`, `images.urlAllowlist`).
|
||||
- Exact host: `"cdn.example.com"`
|
||||
- Wildcard subdomains: `"*.assets.example.com"` (does not match apex)
|
||||
- Empty or omitted allowlists mean no hostname allowlist restriction.
|
||||
- To disable URL-based fetches entirely, set `files.allowUrl: false` and/or `images.allowUrl: false`.
|
||||
|
||||
## File + image limits (config)
|
||||
|
||||
Defaults can be tuned under `gateway.http.endpoints.responses`:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
http: {
|
||||
endpoints: {
|
||||
responses: {
|
||||
enabled: true,
|
||||
maxBodyBytes: 20000000,
|
||||
maxUrlParts: 8,
|
||||
files: {
|
||||
allowUrl: true,
|
||||
urlAllowlist: ["cdn.example.com", "*.assets.example.com"],
|
||||
allowedMimes: [
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
"text/html",
|
||||
"text/csv",
|
||||
"application/json",
|
||||
"application/pdf",
|
||||
],
|
||||
maxBytes: 5242880,
|
||||
maxChars: 200000,
|
||||
maxRedirects: 3,
|
||||
timeoutMs: 10000,
|
||||
pdf: {
|
||||
maxPages: 4,
|
||||
maxPixels: 4000000,
|
||||
minTextChars: 200,
|
||||
},
|
||||
},
|
||||
images: {
|
||||
allowUrl: true,
|
||||
urlAllowlist: ["images.example.com"],
|
||||
allowedMimes: [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/heic",
|
||||
"image/heif",
|
||||
],
|
||||
maxBytes: 10485760,
|
||||
maxRedirects: 3,
|
||||
timeoutMs: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Defaults when omitted:
|
||||
|
||||
- `maxBodyBytes`: 20MB
|
||||
- `maxUrlParts`: 8
|
||||
- `files.maxBytes`: 5MB
|
||||
- `files.maxChars`: 200k
|
||||
- `files.maxRedirects`: 3
|
||||
- `files.timeoutMs`: 10s
|
||||
- `files.pdf.maxPages`: 4
|
||||
- `files.pdf.maxPixels`: 4,000,000
|
||||
- `files.pdf.minTextChars`: 200
|
||||
- `images.maxBytes`: 10MB
|
||||
- `images.maxRedirects`: 3
|
||||
- `images.timeoutMs`: 10s
|
||||
- HEIC/HEIF `input_image` sources are accepted and normalized to JPEG before provider delivery.
|
||||
|
||||
Security note:
|
||||
|
||||
- URL allowlists are enforced before fetch and on redirect hops.
|
||||
- Allowlisting a hostname does not bypass private/internal IP blocking.
|
||||
- For internet-exposed gateways, apply network egress controls in addition to app-level guards.
|
||||
See [Security](/gateway/security).
|
||||
|
||||
## Streaming (SSE)
|
||||
|
||||
Set `stream: true` to receive Server-Sent Events (SSE):
|
||||
|
||||
- `Content-Type: text/event-stream`
|
||||
- Each event line is `event: <type>` and `data: <json>`
|
||||
- Stream ends with `data: [DONE]`
|
||||
|
||||
Event types currently emitted:
|
||||
|
||||
- `response.created`
|
||||
- `response.in_progress`
|
||||
- `response.output_item.added`
|
||||
- `response.content_part.added`
|
||||
- `response.output_text.delta`
|
||||
- `response.output_text.done`
|
||||
- `response.content_part.done`
|
||||
- `response.output_item.done`
|
||||
- `response.completed`
|
||||
- `response.failed` (on error)
|
||||
|
||||
## Usage
|
||||
|
||||
`usage` is populated when the underlying provider reports token counts.
|
||||
OpenClaw normalizes common OpenAI-style aliases before those counters reach
|
||||
downstream status/session surfaces, including `input_tokens` / `output_tokens`
|
||||
and `prompt_tokens` / `completion_tokens`.
|
||||
|
||||
## Errors
|
||||
|
||||
Errors use a JSON object like:
|
||||
|
||||
```json
|
||||
{ "error": { "message": "...", "type": "invalid_request_error" } }
|
||||
```
|
||||
|
||||
Common cases:
|
||||
|
||||
- `401` missing/invalid auth
|
||||
- `400` invalid request body
|
||||
- `405` wrong method
|
||||
|
||||
## Examples
|
||||
|
||||
Non-streaming:
|
||||
|
||||
```bash
|
||||
curl -sS http://127.0.0.1:18789/v1/responses \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'x-openclaw-agent-id: main' \
|
||||
-d '{
|
||||
"model": "openclaw",
|
||||
"input": "hi"
|
||||
}'
|
||||
```
|
||||
|
||||
Streaming:
|
||||
|
||||
```bash
|
||||
curl -N http://127.0.0.1:18789/v1/responses \
|
||||
-H 'Authorization: Bearer YOUR_TOKEN' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'x-openclaw-agent-id: main' \
|
||||
-d '{
|
||||
"model": "openclaw",
|
||||
"stream": true,
|
||||
"input": "hi"
|
||||
}'
|
||||
```
|
||||
307
openclaw/docs/gateway/openshell.md
Normal file
307
openclaw/docs/gateway/openshell.md
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
---
|
||||
title: OpenShell
|
||||
summary: "Use OpenShell as a managed sandbox backend for OpenClaw agents"
|
||||
read_when:
|
||||
- You want cloud-managed sandboxes instead of local Docker
|
||||
- You are setting up the OpenShell plugin
|
||||
- You need to choose between mirror and remote workspace modes
|
||||
---
|
||||
|
||||
# OpenShell
|
||||
|
||||
OpenShell is a managed sandbox backend for OpenClaw. Instead of running Docker
|
||||
containers locally, OpenClaw delegates sandbox lifecycle to the `openshell` CLI,
|
||||
which provisions remote environments with SSH-based command execution.
|
||||
|
||||
The OpenShell plugin reuses the same core SSH transport and remote filesystem
|
||||
bridge as the generic [SSH backend](/gateway/sandboxing#ssh-backend). It adds
|
||||
OpenShell-specific lifecycle (`sandbox create/get/delete`, `sandbox ssh-config`)
|
||||
and an optional `mirror` workspace mode.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The `openshell` CLI installed and on `PATH` (or set a custom path via
|
||||
`plugins.entries.openshell.config.command`)
|
||||
- An OpenShell account with sandbox access
|
||||
- OpenClaw Gateway running on the host
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Enable the plugin and set the sandbox backend:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "session",
|
||||
workspaceAccess: "rw",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
openshell: {
|
||||
enabled: true,
|
||||
config: {
|
||||
from: "openclaw",
|
||||
mode: "remote",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
2. Restart the Gateway. On the next agent turn, OpenClaw creates an OpenShell
|
||||
sandbox and routes tool execution through it.
|
||||
|
||||
3. Verify:
|
||||
|
||||
```bash
|
||||
openclaw sandbox list
|
||||
openclaw sandbox explain
|
||||
```
|
||||
|
||||
## Workspace modes
|
||||
|
||||
This is the most important decision when using OpenShell.
|
||||
|
||||
### `mirror`
|
||||
|
||||
Use `plugins.entries.openshell.config.mode: "mirror"` when you want the **local
|
||||
workspace to stay canonical**.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Before `exec`, OpenClaw syncs the local workspace into the OpenShell sandbox.
|
||||
- After `exec`, OpenClaw syncs the remote workspace back to the local workspace.
|
||||
- File tools still operate through the sandbox bridge, but the local workspace
|
||||
remains the source of truth between turns.
|
||||
|
||||
Best for:
|
||||
|
||||
- You edit files locally outside OpenClaw and want those changes visible in the
|
||||
sandbox automatically.
|
||||
- You want the OpenShell sandbox to behave as much like the Docker backend as
|
||||
possible.
|
||||
- You want the host workspace to reflect sandbox writes after each exec turn.
|
||||
|
||||
Tradeoff: extra sync cost before and after each exec.
|
||||
|
||||
### `remote`
|
||||
|
||||
Use `plugins.entries.openshell.config.mode: "remote"` when you want the
|
||||
**OpenShell workspace to become canonical**.
|
||||
|
||||
Behavior:
|
||||
|
||||
- When the sandbox is first created, OpenClaw seeds the remote workspace from
|
||||
the local workspace once.
|
||||
- After that, `exec`, `read`, `write`, `edit`, and `apply_patch` operate
|
||||
directly against the remote OpenShell workspace.
|
||||
- OpenClaw does **not** sync remote changes back into the local workspace.
|
||||
- Prompt-time media reads still work because file and media tools read through
|
||||
the sandbox bridge.
|
||||
|
||||
Best for:
|
||||
|
||||
- The sandbox should live primarily on the remote side.
|
||||
- You want lower per-turn sync overhead.
|
||||
- You do not want host-local edits to silently overwrite remote sandbox state.
|
||||
|
||||
Important: if you edit files on the host outside OpenClaw after the initial seed,
|
||||
the remote sandbox does **not** see those changes. Use
|
||||
`openclaw sandbox recreate` to re-seed.
|
||||
|
||||
### Choosing a mode
|
||||
|
||||
| | `mirror` | `remote` |
|
||||
| ------------------------ | -------------------------- | ------------------------- |
|
||||
| **Canonical workspace** | Local host | Remote OpenShell |
|
||||
| **Sync direction** | Bidirectional (each exec) | One-time seed |
|
||||
| **Per-turn overhead** | Higher (upload + download) | Lower (direct remote ops) |
|
||||
| **Local edits visible?** | Yes, on next exec | No, until recreate |
|
||||
| **Best for** | Development workflows | Long-running agents, CI |
|
||||
|
||||
## Configuration reference
|
||||
|
||||
All OpenShell config lives under `plugins.entries.openshell.config`:
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| ------------------------- | ------------------------ | ------------- | ----------------------------------------------------- |
|
||||
| `mode` | `"mirror"` or `"remote"` | `"mirror"` | Workspace sync mode |
|
||||
| `command` | `string` | `"openshell"` | Path or name of the `openshell` CLI |
|
||||
| `from` | `string` | `"openclaw"` | Sandbox source for first-time create |
|
||||
| `gateway` | `string` | — | OpenShell gateway name (`--gateway`) |
|
||||
| `gatewayEndpoint` | `string` | — | OpenShell gateway endpoint URL (`--gateway-endpoint`) |
|
||||
| `policy` | `string` | — | OpenShell policy ID for sandbox creation |
|
||||
| `providers` | `string[]` | `[]` | Provider names to attach when sandbox is created |
|
||||
| `gpu` | `boolean` | `false` | Request GPU resources |
|
||||
| `autoProviders` | `boolean` | `true` | Pass `--auto-providers` during sandbox create |
|
||||
| `remoteWorkspaceDir` | `string` | `"/sandbox"` | Primary writable workspace inside the sandbox |
|
||||
| `remoteAgentWorkspaceDir` | `string` | `"/agent"` | Agent workspace mount path (for read-only access) |
|
||||
| `timeoutSeconds` | `number` | `120` | Timeout for `openshell` CLI operations |
|
||||
|
||||
Sandbox-level settings (`mode`, `scope`, `workspaceAccess`) are configured under
|
||||
`agents.defaults.sandbox` as with any backend. See
|
||||
[Sandboxing](/gateway/sandboxing) for the full matrix.
|
||||
|
||||
## Examples
|
||||
|
||||
### Minimal remote setup
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
openshell: {
|
||||
enabled: true,
|
||||
config: {
|
||||
from: "openclaw",
|
||||
mode: "remote",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Mirror mode with GPU
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "agent",
|
||||
workspaceAccess: "rw",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
openshell: {
|
||||
enabled: true,
|
||||
config: {
|
||||
from: "openclaw",
|
||||
mode: "mirror",
|
||||
gpu: true,
|
||||
providers: ["openai"],
|
||||
timeoutSeconds: 180,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Per-agent OpenShell with custom gateway
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: { mode: "off" },
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "researcher",
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "agent",
|
||||
workspaceAccess: "rw",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
openshell: {
|
||||
enabled: true,
|
||||
config: {
|
||||
from: "openclaw",
|
||||
mode: "remote",
|
||||
gateway: "lab",
|
||||
gatewayEndpoint: "https://lab.example",
|
||||
policy: "strict",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle management
|
||||
|
||||
OpenShell sandboxes are managed through the normal sandbox CLI:
|
||||
|
||||
```bash
|
||||
# List all sandbox runtimes (Docker + OpenShell)
|
||||
openclaw sandbox list
|
||||
|
||||
# Inspect effective policy
|
||||
openclaw sandbox explain
|
||||
|
||||
# Recreate (deletes remote workspace, re-seeds on next use)
|
||||
openclaw sandbox recreate --all
|
||||
```
|
||||
|
||||
For `remote` mode, **recreate is especially important**: it deletes the canonical
|
||||
remote workspace for that scope. The next use seeds a fresh remote workspace from
|
||||
the local workspace.
|
||||
|
||||
For `mirror` mode, recreate mainly resets the remote execution environment because
|
||||
the local workspace remains canonical.
|
||||
|
||||
### When to recreate
|
||||
|
||||
Recreate after changing any of these:
|
||||
|
||||
- `agents.defaults.sandbox.backend`
|
||||
- `plugins.entries.openshell.config.from`
|
||||
- `plugins.entries.openshell.config.mode`
|
||||
- `plugins.entries.openshell.config.policy`
|
||||
|
||||
```bash
|
||||
openclaw sandbox recreate --all
|
||||
```
|
||||
|
||||
## Current limitations
|
||||
|
||||
- Sandbox browser is not supported on the OpenShell backend.
|
||||
- `sandbox.docker.binds` does not apply to OpenShell.
|
||||
- Docker-specific runtime knobs under `sandbox.docker.*` apply only to the Docker
|
||||
backend.
|
||||
|
||||
## How it works
|
||||
|
||||
1. OpenClaw calls `openshell sandbox create` (with `--from`, `--gateway`,
|
||||
`--policy`, `--providers`, `--gpu` flags as configured).
|
||||
2. OpenClaw calls `openshell sandbox ssh-config <name>` to get SSH connection
|
||||
details for the sandbox.
|
||||
3. Core writes the SSH config to a temp file and opens an SSH session using the
|
||||
same remote filesystem bridge as the generic SSH backend.
|
||||
4. In `mirror` mode: sync local to remote before exec, run, sync back after exec.
|
||||
5. In `remote` mode: seed once on create, then operate directly on the remote
|
||||
workspace.
|
||||
|
||||
## See also
|
||||
|
||||
- [Sandboxing](/gateway/sandboxing) -- modes, scopes, and backend comparison
|
||||
- [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) -- debugging blocked tools
|
||||
- [Multi-Agent Sandbox and Tools](/tools/multi-agent-sandbox-tools) -- per-agent overrides
|
||||
- [Sandbox CLI](/cli/sandbox) -- `openclaw sandbox` commands
|
||||
138
openclaw/docs/gateway/pairing.md
Normal file
138
openclaw/docs/gateway/pairing.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
---
|
||||
summary: "Gateway-owned node pairing (Option B) for iOS and other remote nodes"
|
||||
read_when:
|
||||
- Implementing node pairing approvals without macOS UI
|
||||
- Adding CLI flows for approving remote nodes
|
||||
- Extending gateway protocol with node management
|
||||
title: "Gateway-Owned Pairing"
|
||||
---
|
||||
|
||||
# Gateway-owned pairing (Option B)
|
||||
|
||||
In Gateway-owned pairing, the **Gateway** is the source of truth for which nodes
|
||||
are allowed to join. UIs (macOS app, future clients) are just frontends that
|
||||
approve or reject pending requests.
|
||||
|
||||
**Important:** WS nodes use **device pairing** (role `node`) during `connect`.
|
||||
`node.pair.*` is a separate pairing store and does **not** gate the WS handshake.
|
||||
Only clients that explicitly call `node.pair.*` use this flow.
|
||||
|
||||
## Concepts
|
||||
|
||||
- **Pending request**: a node asked to join; requires approval.
|
||||
- **Paired node**: approved node with an issued auth token.
|
||||
- **Transport**: the Gateway WS endpoint forwards requests but does not decide
|
||||
membership. (Legacy TCP bridge support has been removed.)
|
||||
|
||||
## How pairing works
|
||||
|
||||
1. A node connects to the Gateway WS and requests pairing.
|
||||
2. The Gateway stores a **pending request** and emits `node.pair.requested`.
|
||||
3. You approve or reject the request (CLI or UI).
|
||||
4. On approval, the Gateway issues a **new token** (tokens are rotated on re‑pair).
|
||||
5. The node reconnects using the token and is now “paired”.
|
||||
|
||||
Pending requests expire automatically after **5 minutes**.
|
||||
|
||||
## CLI workflow (headless friendly)
|
||||
|
||||
```bash
|
||||
openclaw nodes pending
|
||||
openclaw nodes approve <requestId>
|
||||
openclaw nodes reject <requestId>
|
||||
openclaw nodes status
|
||||
openclaw nodes rename --node <id|name|ip> --name "Living Room iPad"
|
||||
```
|
||||
|
||||
`nodes status` shows paired/connected nodes and their capabilities.
|
||||
|
||||
## API surface (gateway protocol)
|
||||
|
||||
Events:
|
||||
|
||||
- `node.pair.requested` — emitted when a new pending request is created.
|
||||
- `node.pair.resolved` — emitted when a request is approved/rejected/expired.
|
||||
|
||||
Methods:
|
||||
|
||||
- `node.pair.request` — create or reuse a pending request.
|
||||
- `node.pair.list` — list pending + paired nodes (`operator.pairing`).
|
||||
- `node.pair.approve` — approve a pending request (issues token).
|
||||
- `node.pair.reject` — reject a pending request.
|
||||
- `node.pair.verify` — verify `{ nodeId, token }`.
|
||||
|
||||
Notes:
|
||||
|
||||
- `node.pair.request` is idempotent per node: repeated calls return the same
|
||||
pending request.
|
||||
- Repeated requests for the same pending node also refresh the stored node
|
||||
metadata and the latest allowlisted declared command snapshot for operator visibility.
|
||||
- Approval **always** generates a fresh token; no token is ever returned from
|
||||
`node.pair.request`.
|
||||
- Requests may include `silent: true` as a hint for auto-approval flows.
|
||||
- `node.pair.approve` uses the pending request's declared commands to enforce
|
||||
extra approval scopes:
|
||||
- commandless request: `operator.pairing`
|
||||
- non-exec command request: `operator.pairing` + `operator.write`
|
||||
- `system.run` / `system.run.prepare` / `system.which` request:
|
||||
`operator.pairing` + `operator.admin`
|
||||
|
||||
Important:
|
||||
|
||||
- Node pairing is a trust/identity flow plus token issuance.
|
||||
- It does **not** pin the live node command surface per node.
|
||||
- Live node commands come from what the node declares on connect after the
|
||||
gateway's global node command policy (`gateway.nodes.allowCommands` /
|
||||
`denyCommands`) is applied.
|
||||
- Per-node `system.run` allow/ask policy lives on the node in
|
||||
`exec.approvals.node.*`, not in the pairing record.
|
||||
|
||||
## Node command gating (2026.3.31+)
|
||||
|
||||
<Warning>
|
||||
**Breaking change:** Starting with `2026.3.31`, node commands are disabled until node pairing is approved. Device pairing alone is no longer enough to expose declared node commands.
|
||||
</Warning>
|
||||
|
||||
When a node connects for the first time, pairing is requested automatically. Until the pairing request is approved, all pending node commands from that node are filtered and will not execute. Once trust is established through pairing approval, the node's declared commands become available subject to the normal command policy.
|
||||
|
||||
This means:
|
||||
|
||||
- Nodes that were previously relying on device pairing alone to expose commands must now complete node pairing.
|
||||
- Commands queued before pairing approval are dropped, not deferred.
|
||||
|
||||
## Node event trust boundaries (2026.3.31+)
|
||||
|
||||
<Warning>
|
||||
**Breaking change:** Node-originated runs now stay on a reduced trusted surface.
|
||||
</Warning>
|
||||
|
||||
Node-originated summaries and related session events are restricted to the intended trusted surface. Notification-driven or node-triggered flows that previously relied on broader host or session tool access may need adjustment. This hardening ensures that node events cannot escalate into host-level tool access beyond what the node's trust boundary permits.
|
||||
|
||||
## Auto-approval (macOS app)
|
||||
|
||||
The macOS app can optionally attempt a **silent approval** when:
|
||||
|
||||
- the request is marked `silent`, and
|
||||
- the app can verify an SSH connection to the gateway host using the same user.
|
||||
|
||||
If silent approval fails, it falls back to the normal “Approve/Reject” prompt.
|
||||
|
||||
## Storage (local, private)
|
||||
|
||||
Pairing state is stored under the Gateway state directory (default `~/.openclaw`):
|
||||
|
||||
- `~/.openclaw/nodes/paired.json`
|
||||
- `~/.openclaw/nodes/pending.json`
|
||||
|
||||
If you override `OPENCLAW_STATE_DIR`, the `nodes/` folder moves with it.
|
||||
|
||||
Security notes:
|
||||
|
||||
- Tokens are secrets; treat `paired.json` as sensitive.
|
||||
- Rotating a token requires re-approval (or deleting the node entry).
|
||||
|
||||
## Transport behavior
|
||||
|
||||
- The transport is **stateless**; it does not store membership.
|
||||
- If the Gateway is offline or pairing is disabled, nodes cannot pair.
|
||||
- If the Gateway is in remote mode, pairing still happens against the remote Gateway’s store.
|
||||
663
openclaw/docs/gateway/protocol.md
Normal file
663
openclaw/docs/gateway/protocol.md
Normal file
|
|
@ -0,0 +1,663 @@
|
|||
---
|
||||
summary: "Gateway WebSocket protocol: handshake, frames, versioning"
|
||||
read_when:
|
||||
- Implementing or updating gateway WS clients
|
||||
- Debugging protocol mismatches or connect failures
|
||||
- Regenerating protocol schema/models
|
||||
title: "Gateway Protocol"
|
||||
---
|
||||
|
||||
# Gateway protocol (WebSocket)
|
||||
|
||||
The Gateway WS protocol is the **single control plane + node transport** for
|
||||
OpenClaw. All clients (CLI, web UI, macOS app, iOS/Android nodes, headless
|
||||
nodes) connect over WebSocket and declare their **role** + **scope** at
|
||||
handshake time.
|
||||
|
||||
## Transport
|
||||
|
||||
- WebSocket, text frames with JSON payloads.
|
||||
- First frame **must** be a `connect` request.
|
||||
|
||||
## Handshake (connect)
|
||||
|
||||
Gateway → Client (pre-connect challenge):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "event",
|
||||
"event": "connect.challenge",
|
||||
"payload": { "nonce": "…", "ts": 1737264000000 }
|
||||
}
|
||||
```
|
||||
|
||||
Client → Gateway:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "req",
|
||||
"id": "…",
|
||||
"method": "connect",
|
||||
"params": {
|
||||
"minProtocol": 3,
|
||||
"maxProtocol": 3,
|
||||
"client": {
|
||||
"id": "cli",
|
||||
"version": "1.2.3",
|
||||
"platform": "macos",
|
||||
"mode": "operator"
|
||||
},
|
||||
"role": "operator",
|
||||
"scopes": ["operator.read", "operator.write"],
|
||||
"caps": [],
|
||||
"commands": [],
|
||||
"permissions": {},
|
||||
"auth": { "token": "…" },
|
||||
"locale": "en-US",
|
||||
"userAgent": "openclaw-cli/1.2.3",
|
||||
"device": {
|
||||
"id": "device_fingerprint",
|
||||
"publicKey": "…",
|
||||
"signature": "…",
|
||||
"signedAt": 1737264000000,
|
||||
"nonce": "…"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Gateway → Client:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "res",
|
||||
"id": "…",
|
||||
"ok": true,
|
||||
"payload": {
|
||||
"type": "hello-ok",
|
||||
"protocol": 3,
|
||||
"server": { "version": "…", "connId": "…" },
|
||||
"features": { "methods": ["…"], "events": ["…"] },
|
||||
"snapshot": { "…": "…" },
|
||||
"policy": {
|
||||
"maxPayload": 26214400,
|
||||
"maxBufferedBytes": 52428800,
|
||||
"tickIntervalMs": 15000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`server`, `features`, `snapshot`, and `policy` are all required by the schema
|
||||
(`src/gateway/protocol/schema/frames.ts`). `canvasHostUrl` is optional. `auth`
|
||||
reports the negotiated role/scopes when available, and includes `deviceToken`
|
||||
when the gateway issues one.
|
||||
|
||||
When no device token is issued, `hello-ok.auth` can still report the negotiated
|
||||
permissions:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"role": "operator",
|
||||
"scopes": ["operator.read", "operator.write"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When a device token is issued, `hello-ok` also includes:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"deviceToken": "…",
|
||||
"role": "operator",
|
||||
"scopes": ["operator.read", "operator.write"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
During trusted bootstrap handoff, `hello-ok.auth` may also include additional
|
||||
bounded role entries in `deviceTokens`:
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": {
|
||||
"deviceToken": "…",
|
||||
"role": "node",
|
||||
"scopes": [],
|
||||
"deviceTokens": [
|
||||
{
|
||||
"deviceToken": "…",
|
||||
"role": "operator",
|
||||
"scopes": ["operator.approvals", "operator.read", "operator.talk.secrets", "operator.write"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For the built-in node/operator bootstrap flow, the primary node token stays
|
||||
`scopes: []` and any handed-off operator token stays bounded to the bootstrap
|
||||
operator allowlist (`operator.approvals`, `operator.read`,
|
||||
`operator.talk.secrets`, `operator.write`). Bootstrap scope checks stay
|
||||
role-prefixed: operator entries only satisfy operator requests, and non-operator
|
||||
roles still need scopes under their own role prefix.
|
||||
|
||||
### Node example
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "req",
|
||||
"id": "…",
|
||||
"method": "connect",
|
||||
"params": {
|
||||
"minProtocol": 3,
|
||||
"maxProtocol": 3,
|
||||
"client": {
|
||||
"id": "ios-node",
|
||||
"version": "1.2.3",
|
||||
"platform": "ios",
|
||||
"mode": "node"
|
||||
},
|
||||
"role": "node",
|
||||
"scopes": [],
|
||||
"caps": ["camera", "canvas", "screen", "location", "voice"],
|
||||
"commands": ["camera.snap", "canvas.navigate", "screen.record", "location.get"],
|
||||
"permissions": { "camera.capture": true, "screen.record": false },
|
||||
"auth": { "token": "…" },
|
||||
"locale": "en-US",
|
||||
"userAgent": "openclaw-ios/1.2.3",
|
||||
"device": {
|
||||
"id": "device_fingerprint",
|
||||
"publicKey": "…",
|
||||
"signature": "…",
|
||||
"signedAt": 1737264000000,
|
||||
"nonce": "…"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Framing
|
||||
|
||||
- **Request**: `{type:"req", id, method, params}`
|
||||
- **Response**: `{type:"res", id, ok, payload|error}`
|
||||
- **Event**: `{type:"event", event, payload, seq?, stateVersion?}`
|
||||
|
||||
Side-effecting methods require **idempotency keys** (see schema).
|
||||
|
||||
## Roles + scopes
|
||||
|
||||
### Roles
|
||||
|
||||
- `operator` = control plane client (CLI/UI/automation).
|
||||
- `node` = capability host (camera/screen/canvas/system.run).
|
||||
|
||||
### Scopes (operator)
|
||||
|
||||
Common scopes:
|
||||
|
||||
- `operator.read`
|
||||
- `operator.write`
|
||||
- `operator.admin`
|
||||
- `operator.approvals`
|
||||
- `operator.pairing`
|
||||
- `operator.talk.secrets`
|
||||
|
||||
`talk.config` with `includeSecrets: true` requires `operator.talk.secrets`
|
||||
(or `operator.admin`).
|
||||
|
||||
Plugin-registered gateway RPC methods may request their own operator scope, but
|
||||
reserved core admin prefixes (`config.*`, `exec.approvals.*`, `wizard.*`,
|
||||
`update.*`) always resolve to `operator.admin`.
|
||||
|
||||
Method scope is only the first gate. Some slash commands reached through
|
||||
`chat.send` apply stricter command-level checks on top. For example, persistent
|
||||
`/config set` and `/config unset` writes require `operator.admin`.
|
||||
|
||||
`node.pair.approve` also has an extra approval-time scope check on top of the
|
||||
base method scope:
|
||||
|
||||
- commandless requests: `operator.pairing`
|
||||
- requests with non-exec node commands: `operator.pairing` + `operator.write`
|
||||
- requests that include `system.run`, `system.run.prepare`, or `system.which`:
|
||||
`operator.pairing` + `operator.admin`
|
||||
|
||||
### Caps/commands/permissions (node)
|
||||
|
||||
Nodes declare capability claims at connect time:
|
||||
|
||||
- `caps`: high-level capability categories.
|
||||
- `commands`: command allowlist for invoke.
|
||||
- `permissions`: granular toggles (e.g. `screen.record`, `camera.capture`).
|
||||
|
||||
The Gateway treats these as **claims** and enforces server-side allowlists.
|
||||
|
||||
## Presence
|
||||
|
||||
- `system-presence` returns entries keyed by device identity.
|
||||
- Presence entries include `deviceId`, `roles`, and `scopes` so UIs can show a single row per device
|
||||
even when it connects as both **operator** and **node**.
|
||||
|
||||
## Common RPC method families
|
||||
|
||||
This page is not a generated full dump, but the public WS surface is broader
|
||||
than the handshake/auth examples above. These are the main method families the
|
||||
Gateway exposes today.
|
||||
|
||||
`hello-ok.features.methods` is a conservative discovery list built from
|
||||
`src/gateway/server-methods-list.ts` plus loaded plugin/channel method exports.
|
||||
Treat it as feature discovery, not as a generated dump of every callable helper
|
||||
implemented in `src/gateway/server-methods/*.ts`.
|
||||
|
||||
### System and identity
|
||||
|
||||
- `health` returns the cached or freshly probed gateway health snapshot.
|
||||
- `status` returns the `/status`-style gateway summary; sensitive fields are
|
||||
included only for admin-scoped operator clients.
|
||||
- `gateway.identity.get` returns the gateway device identity used by relay and
|
||||
pairing flows.
|
||||
- `system-presence` returns the current presence snapshot for connected
|
||||
operator/node devices.
|
||||
- `system-event` appends a system event and can update/broadcast presence
|
||||
context.
|
||||
- `last-heartbeat` returns the latest persisted heartbeat event.
|
||||
- `set-heartbeats` toggles heartbeat processing on the gateway.
|
||||
|
||||
### Models and usage
|
||||
|
||||
- `models.list` returns the runtime-allowed model catalog.
|
||||
- `usage.status` returns provider usage windows/remaining quota summaries.
|
||||
- `usage.cost` returns aggregated cost usage summaries for a date range.
|
||||
- `doctor.memory.status` returns vector-memory / embedding readiness for the
|
||||
active default agent workspace.
|
||||
- `sessions.usage` returns per-session usage summaries.
|
||||
- `sessions.usage.timeseries` returns timeseries usage for one session.
|
||||
- `sessions.usage.logs` returns usage log entries for one session.
|
||||
|
||||
### Channels and login helpers
|
||||
|
||||
- `channels.status` returns built-in + bundled channel/plugin status summaries.
|
||||
- `channels.logout` logs out a specific channel/account where the channel
|
||||
supports logout.
|
||||
- `web.login.start` starts a QR/web login flow for the current QR-capable web
|
||||
channel provider.
|
||||
- `web.login.wait` waits for that QR/web login flow to complete and starts the
|
||||
channel on success.
|
||||
- `push.test` sends a test APNs push to a registered iOS node.
|
||||
- `voicewake.get` returns the stored wake-word triggers.
|
||||
- `voicewake.set` updates wake-word triggers and broadcasts the change.
|
||||
|
||||
### Messaging and logs
|
||||
|
||||
- `send` is the direct outbound-delivery RPC for channel/account/thread-targeted
|
||||
sends outside the chat runner.
|
||||
- `logs.tail` returns the configured gateway file-log tail with cursor/limit and
|
||||
max-byte controls.
|
||||
|
||||
### Talk and TTS
|
||||
|
||||
- `talk.config` returns the effective Talk config payload; `includeSecrets`
|
||||
requires `operator.talk.secrets` (or `operator.admin`).
|
||||
- `talk.mode` sets/broadcasts the current Talk mode state for WebChat/Control UI
|
||||
clients.
|
||||
- `talk.speak` synthesizes speech through the active Talk speech provider.
|
||||
- `tts.status` returns TTS enabled state, active provider, fallback providers,
|
||||
and provider config state.
|
||||
- `tts.providers` returns the visible TTS provider inventory.
|
||||
- `tts.enable` and `tts.disable` toggle TTS prefs state.
|
||||
- `tts.setProvider` updates the preferred TTS provider.
|
||||
- `tts.convert` runs one-shot text-to-speech conversion.
|
||||
|
||||
### Secrets, config, update, and wizard
|
||||
|
||||
- `secrets.reload` re-resolves active SecretRefs and swaps runtime secret state
|
||||
only on full success.
|
||||
- `secrets.resolve` resolves command-target secret assignments for a specific
|
||||
command/target set.
|
||||
- `config.get` returns the current config snapshot and hash.
|
||||
- `config.set` writes a validated config payload.
|
||||
- `config.patch` merges a partial config update.
|
||||
- `config.apply` validates + replaces the full config payload.
|
||||
- `config.schema` returns the live config schema payload used by Control UI and
|
||||
CLI tooling: schema, `uiHints`, version, and generation metadata, including
|
||||
plugin + channel schema metadata when the runtime can load it. The schema
|
||||
includes field `title` / `description` metadata derived from the same labels
|
||||
and help text used by the UI, including nested object, wildcard, array-item,
|
||||
and `anyOf` / `oneOf` / `allOf` composition branches when matching field
|
||||
documentation exists.
|
||||
- `config.schema.lookup` returns a path-scoped lookup payload for one config
|
||||
path: normalized path, a shallow schema node, matched hint + `hintPath`, and
|
||||
immediate child summaries for UI/CLI drill-down.
|
||||
- Lookup schema nodes keep the user-facing docs and common validation fields:
|
||||
`title`, `description`, `type`, `enum`, `const`, `format`, `pattern`,
|
||||
numeric/string/array/object bounds, and boolean flags like
|
||||
`additionalProperties`, `deprecated`, `readOnly`, `writeOnly`.
|
||||
- Child summaries expose `key`, normalized `path`, `type`, `required`,
|
||||
`hasChildren`, plus the matched `hint` / `hintPath`.
|
||||
- `update.run` runs the gateway update flow and schedules a restart only when
|
||||
the update itself succeeded.
|
||||
- `wizard.start`, `wizard.next`, `wizard.status`, and `wizard.cancel` expose the
|
||||
onboarding wizard over WS RPC.
|
||||
|
||||
### Existing major families
|
||||
|
||||
#### Agent and workspace helpers
|
||||
|
||||
- `agents.list` returns configured agent entries.
|
||||
- `agents.create`, `agents.update`, and `agents.delete` manage agent records and
|
||||
workspace wiring.
|
||||
- `agents.files.list`, `agents.files.get`, and `agents.files.set` manage the
|
||||
bootstrap workspace files exposed for an agent.
|
||||
- `agent.identity.get` returns the effective assistant identity for an agent or
|
||||
session.
|
||||
- `agent.wait` waits for a run to finish and returns the terminal snapshot when
|
||||
available.
|
||||
|
||||
#### Session control
|
||||
|
||||
- `sessions.list` returns the current session index.
|
||||
- `sessions.subscribe` and `sessions.unsubscribe` toggle session change event
|
||||
subscriptions for the current WS client.
|
||||
- `sessions.messages.subscribe` and `sessions.messages.unsubscribe` toggle
|
||||
transcript/message event subscriptions for one session.
|
||||
- `sessions.preview` returns bounded transcript previews for specific session
|
||||
keys.
|
||||
- `sessions.resolve` resolves or canonicalizes a session target.
|
||||
- `sessions.create` creates a new session entry.
|
||||
- `sessions.send` sends a message into an existing session.
|
||||
- `sessions.steer` is the interrupt-and-steer variant for an active session.
|
||||
- `sessions.abort` aborts active work for a session.
|
||||
- `sessions.patch` updates session metadata/overrides.
|
||||
- `sessions.reset`, `sessions.delete`, and `sessions.compact` perform session
|
||||
maintenance.
|
||||
- `sessions.get` returns the full stored session row.
|
||||
- chat execution still uses `chat.history`, `chat.send`, `chat.abort`, and
|
||||
`chat.inject`.
|
||||
- `chat.history` is display-normalized for UI clients: inline directive tags are
|
||||
stripped from visible text, plain-text tool-call XML payloads (including
|
||||
`<tool_call>...</tool_call>`, `<function_call>...</function_call>`,
|
||||
`<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and
|
||||
truncated tool-call blocks) and leaked ASCII/full-width model control tokens
|
||||
are stripped, pure silent-token assistant rows such as exact `NO_REPLY` /
|
||||
`no_reply` are omitted, and oversized rows can be replaced with placeholders.
|
||||
|
||||
#### Device pairing and device tokens
|
||||
|
||||
- `device.pair.list` returns pending and approved paired devices.
|
||||
- `device.pair.approve`, `device.pair.reject`, and `device.pair.remove` manage
|
||||
device-pairing records.
|
||||
- `device.token.rotate` rotates a paired device token within its approved role
|
||||
and scope bounds.
|
||||
- `device.token.revoke` revokes a paired device token.
|
||||
|
||||
#### Node pairing, invoke, and pending work
|
||||
|
||||
- `node.pair.request`, `node.pair.list`, `node.pair.approve`,
|
||||
`node.pair.reject`, and `node.pair.verify` cover node pairing and bootstrap
|
||||
verification.
|
||||
- `node.list` and `node.describe` return known/connected node state.
|
||||
- `node.rename` updates a paired node label.
|
||||
- `node.invoke` forwards a command to a connected node.
|
||||
- `node.invoke.result` returns the result for an invoke request.
|
||||
- `node.event` carries node-originated events back into the gateway.
|
||||
- `node.canvas.capability.refresh` refreshes scoped canvas-capability tokens.
|
||||
- `node.pending.pull` and `node.pending.ack` are the connected-node queue APIs.
|
||||
- `node.pending.enqueue` and `node.pending.drain` manage durable pending work
|
||||
for offline/disconnected nodes.
|
||||
|
||||
#### Approval families
|
||||
|
||||
- `exec.approval.request`, `exec.approval.get`, `exec.approval.list`, and
|
||||
`exec.approval.resolve` cover one-shot exec approval requests plus pending
|
||||
approval lookup/replay.
|
||||
- `exec.approval.waitDecision` waits on one pending exec approval and returns
|
||||
the final decision (or `null` on timeout).
|
||||
- `exec.approvals.get` and `exec.approvals.set` manage gateway exec approval
|
||||
policy snapshots.
|
||||
- `exec.approvals.node.get` and `exec.approvals.node.set` manage node-local exec
|
||||
approval policy via node relay commands.
|
||||
- `plugin.approval.request`, `plugin.approval.list`,
|
||||
`plugin.approval.waitDecision`, and `plugin.approval.resolve` cover
|
||||
plugin-defined approval flows.
|
||||
|
||||
#### Other major families
|
||||
|
||||
- automation:
|
||||
- `wake` schedules an immediate or next-heartbeat wake text injection
|
||||
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`,
|
||||
`cron.run`, `cron.runs`
|
||||
- skills/tools: `commands.list`, `skills.*`, `tools.catalog`, `tools.effective`
|
||||
|
||||
### Common event families
|
||||
|
||||
- `chat`: UI chat updates such as `chat.inject` and other transcript-only chat
|
||||
events.
|
||||
- `session.message` and `session.tool`: transcript/event-stream updates for a
|
||||
subscribed session.
|
||||
- `sessions.changed`: session index or metadata changed.
|
||||
- `presence`: system presence snapshot updates.
|
||||
- `tick`: periodic keepalive / liveness event.
|
||||
- `health`: gateway health snapshot update.
|
||||
- `heartbeat`: heartbeat event stream update.
|
||||
- `cron`: cron run/job change event.
|
||||
- `shutdown`: gateway shutdown notification.
|
||||
- `node.pair.requested` / `node.pair.resolved`: node pairing lifecycle.
|
||||
- `node.invoke.request`: node invoke request broadcast.
|
||||
- `device.pair.requested` / `device.pair.resolved`: paired-device lifecycle.
|
||||
- `voicewake.changed`: wake-word trigger config changed.
|
||||
- `exec.approval.requested` / `exec.approval.resolved`: exec approval
|
||||
lifecycle.
|
||||
- `plugin.approval.requested` / `plugin.approval.resolved`: plugin approval
|
||||
lifecycle.
|
||||
|
||||
### Node helper methods
|
||||
|
||||
- Nodes may call `skills.bins` to fetch the current list of skill executables
|
||||
for auto-allow checks.
|
||||
|
||||
### Operator helper methods
|
||||
|
||||
- Operators may call `commands.list` (`operator.read`) to fetch the runtime
|
||||
command inventory for an agent.
|
||||
- `agentId` is optional; omit it to read the default agent workspace.
|
||||
- `scope` controls which surface the primary `name` targets:
|
||||
- `text` returns the primary text command token without the leading `/`
|
||||
- `native` and the default `both` path return provider-aware native names
|
||||
when available
|
||||
- `textAliases` carries exact slash aliases such as `/model` and `/m`.
|
||||
- `nativeName` carries the provider-aware native command name when one exists.
|
||||
- `provider` is optional and only affects native naming plus native plugin
|
||||
command availability.
|
||||
- `includeArgs=false` omits serialized argument metadata from the response.
|
||||
- Operators may call `tools.catalog` (`operator.read`) to fetch the runtime tool catalog for an
|
||||
agent. The response includes grouped tools and provenance metadata:
|
||||
- `source`: `core` or `plugin`
|
||||
- `pluginId`: plugin owner when `source="plugin"`
|
||||
- `optional`: whether a plugin tool is optional
|
||||
- Operators may call `tools.effective` (`operator.read`) to fetch the runtime-effective tool
|
||||
inventory for a session.
|
||||
- `sessionKey` is required.
|
||||
- The gateway derives trusted runtime context from the session server-side instead of accepting
|
||||
caller-supplied auth or delivery context.
|
||||
- The response is session-scoped and reflects what the active conversation can use right now,
|
||||
including core, plugin, and channel tools.
|
||||
- Operators may call `skills.status` (`operator.read`) to fetch the visible
|
||||
skill inventory for an agent.
|
||||
- `agentId` is optional; omit it to read the default agent workspace.
|
||||
- The response includes eligibility, missing requirements, config checks, and
|
||||
sanitized install options without exposing raw secret values.
|
||||
- Operators may call `skills.search` and `skills.detail` (`operator.read`) for
|
||||
ClawHub discovery metadata.
|
||||
- Operators may call `skills.install` (`operator.admin`) in two modes:
|
||||
- ClawHub mode: `{ source: "clawhub", slug, version?, force? }` installs a
|
||||
skill folder into the default agent workspace `skills/` directory.
|
||||
- Gateway installer mode: `{ name, installId, dangerouslyForceUnsafeInstall?, timeoutMs? }`
|
||||
runs a declared `metadata.openclaw.install` action on the gateway host.
|
||||
- Operators may call `skills.update` (`operator.admin`) in two modes:
|
||||
- ClawHub mode updates one tracked slug or all tracked ClawHub installs in
|
||||
the default agent workspace.
|
||||
- Config mode patches `skills.entries.<skillKey>` values such as `enabled`,
|
||||
`apiKey`, and `env`.
|
||||
|
||||
## Exec approvals
|
||||
|
||||
- When an exec request needs approval, the gateway broadcasts `exec.approval.requested`.
|
||||
- Operator clients resolve by calling `exec.approval.resolve` (requires `operator.approvals` scope).
|
||||
- For `host=node`, `exec.approval.request` must include `systemRunPlan` (canonical `argv`/`cwd`/`rawCommand`/session metadata). Requests missing `systemRunPlan` are rejected.
|
||||
- After approval, forwarded `node.invoke system.run` calls reuse that canonical
|
||||
`systemRunPlan` as the authoritative command/cwd/session context.
|
||||
- If a caller mutates `command`, `rawCommand`, `cwd`, `agentId`, or
|
||||
`sessionKey` between prepare and the final approved `system.run` forward, the
|
||||
gateway rejects the run instead of trusting the mutated payload.
|
||||
|
||||
## Agent delivery fallback
|
||||
|
||||
- `agent` requests can include `deliver=true` to request outbound delivery.
|
||||
- `bestEffortDeliver=false` keeps strict behavior: unresolved or internal-only delivery targets return `INVALID_REQUEST`.
|
||||
- `bestEffortDeliver=true` allows fallback to session-only execution when no external deliverable route can be resolved (for example internal/webchat sessions or ambiguous multi-channel configs).
|
||||
|
||||
## Versioning
|
||||
|
||||
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema/protocol-schemas.ts`.
|
||||
- Clients send `minProtocol` + `maxProtocol`; the server rejects mismatches.
|
||||
- Schemas + models are generated from TypeBox definitions:
|
||||
- `pnpm protocol:gen`
|
||||
- `pnpm protocol:gen:swift`
|
||||
- `pnpm protocol:check`
|
||||
|
||||
### Client constants
|
||||
|
||||
The reference client in `src/gateway/client.ts` uses these defaults. Values are
|
||||
stable across protocol v3 and are the expected baseline for third-party clients.
|
||||
|
||||
| Constant | Default | Source |
|
||||
| ----------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| `PROTOCOL_VERSION` | `3` | `src/gateway/protocol/schema/protocol-schemas.ts` |
|
||||
| Request timeout (per RPC) | `30_000` ms | `src/gateway/client.ts` (`requestTimeoutMs`) |
|
||||
| Preauth / connect-challenge timeout | `10_000` ms | `src/gateway/handshake-timeouts.ts` (clamp `250`–`10_000`) |
|
||||
| Initial reconnect backoff | `1_000` ms | `src/gateway/client.ts` (`backoffMs`) |
|
||||
| Max reconnect backoff | `30_000` ms | `src/gateway/client.ts` (`scheduleReconnect`) |
|
||||
| Fast-retry clamp after device-token close | `250` ms | `src/gateway/client.ts` |
|
||||
| Force-stop grace before `terminate()` | `250` ms | `FORCE_STOP_TERMINATE_GRACE_MS` |
|
||||
| `stopAndWait()` default timeout | `1_000` ms | `STOP_AND_WAIT_TIMEOUT_MS` |
|
||||
| Default tick interval (pre `hello-ok`) | `30_000` ms | `src/gateway/client.ts` |
|
||||
| Tick-timeout close | code `4000` when silence exceeds `tickIntervalMs * 2` | `src/gateway/client.ts` |
|
||||
| `MAX_PAYLOAD_BYTES` | `25 * 1024 * 1024` (25 MB) | `src/gateway/server-constants.ts` |
|
||||
|
||||
The server advertises the effective `policy.tickIntervalMs`, `policy.maxPayload`,
|
||||
and `policy.maxBufferedBytes` in `hello-ok`; clients should honor those values
|
||||
rather than the pre-handshake defaults.
|
||||
|
||||
## Auth
|
||||
|
||||
- Shared-secret gateway auth uses `connect.params.auth.token` or
|
||||
`connect.params.auth.password`, depending on the configured auth mode.
|
||||
- Identity-bearing modes such as Tailscale Serve
|
||||
(`gateway.auth.allowTailscale: true`) or non-loopback
|
||||
`gateway.auth.mode: "trusted-proxy"` satisfy the connect auth check from
|
||||
request headers instead of `connect.params.auth.*`.
|
||||
- Private-ingress `gateway.auth.mode: "none"` skips shared-secret connect auth
|
||||
entirely; do not expose that mode on public/untrusted ingress.
|
||||
- After pairing, the Gateway issues a **device token** scoped to the connection
|
||||
role + scopes. It is returned in `hello-ok.auth.deviceToken` and should be
|
||||
persisted by the client for future connects.
|
||||
- Clients should persist the primary `hello-ok.auth.deviceToken` after any
|
||||
successful connect.
|
||||
- Reconnecting with that **stored** device token should also reuse the stored
|
||||
approved scope set for that token. This preserves read/probe/status access
|
||||
that was already granted and avoids silently collapsing reconnects to a
|
||||
narrower implicit admin-only scope.
|
||||
- Client-side connect auth assembly (`selectConnectAuth` in
|
||||
`src/gateway/client.ts`):
|
||||
- `auth.password` is orthogonal and is always forwarded when set.
|
||||
- `auth.token` is populated in priority order: explicit shared token first,
|
||||
then an explicit `deviceToken`, then a stored per-device token (keyed by
|
||||
`deviceId` + `role`).
|
||||
- `auth.bootstrapToken` is sent only when none of the above resolved an
|
||||
`auth.token`. A shared token or any resolved device token suppresses it.
|
||||
- Auto-promotion of a stored device token on the one-shot
|
||||
`AUTH_TOKEN_MISMATCH` retry is gated to **trusted endpoints only** —
|
||||
loopback, or `wss://` with a pinned `tlsFingerprint`. Public `wss://`
|
||||
without pinning does not qualify.
|
||||
- Additional `hello-ok.auth.deviceTokens` entries are bootstrap handoff tokens.
|
||||
Persist them only when the connect used bootstrap auth on a trusted transport
|
||||
such as `wss://` or loopback/local pairing.
|
||||
- If a client supplies an **explicit** `deviceToken` or explicit `scopes`, that
|
||||
caller-requested scope set remains authoritative; cached scopes are only
|
||||
reused when the client is reusing the stored per-device token.
|
||||
- Device tokens can be rotated/revoked via `device.token.rotate` and
|
||||
`device.token.revoke` (requires `operator.pairing` scope).
|
||||
- Token issuance/rotation stays bounded to the approved role set recorded in
|
||||
that device's pairing entry; rotating a token cannot expand the device into a
|
||||
role that pairing approval never granted.
|
||||
- For paired-device token sessions, device management is self-scoped unless the
|
||||
caller also has `operator.admin`: non-admin callers can remove/revoke/rotate
|
||||
only their **own** device entry.
|
||||
- `device.token.rotate` also checks the requested operator scope set against the
|
||||
caller's current session scopes. Non-admin callers cannot rotate a token into
|
||||
a broader operator scope set than they already hold.
|
||||
- Auth failures include `error.details.code` plus recovery hints:
|
||||
- `error.details.canRetryWithDeviceToken` (boolean)
|
||||
- `error.details.recommendedNextStep` (`retry_with_device_token`, `update_auth_configuration`, `update_auth_credentials`, `wait_then_retry`, `review_auth_configuration`)
|
||||
- Client behavior for `AUTH_TOKEN_MISMATCH`:
|
||||
- Trusted clients may attempt one bounded retry with a cached per-device token.
|
||||
- If that retry fails, clients should stop automatic reconnect loops and surface operator action guidance.
|
||||
|
||||
## Device identity + pairing
|
||||
|
||||
- Nodes should include a stable device identity (`device.id`) derived from a
|
||||
keypair fingerprint.
|
||||
- Gateways issue tokens per device + role.
|
||||
- Pairing approvals are required for new device IDs unless local auto-approval
|
||||
is enabled.
|
||||
- Pairing auto-approval is centered on direct local loopback connects.
|
||||
- OpenClaw also has a narrow backend/container-local self-connect path for
|
||||
trusted shared-secret helper flows.
|
||||
- Same-host tailnet or LAN connects are still treated as remote for pairing and
|
||||
require approval.
|
||||
- All WS clients must include `device` identity during `connect` (operator + node).
|
||||
Control UI can omit it only in these modes:
|
||||
- `gateway.controlUi.allowInsecureAuth=true` for localhost-only insecure HTTP compatibility.
|
||||
- successful `gateway.auth.mode: "trusted-proxy"` operator Control UI auth.
|
||||
- `gateway.controlUi.dangerouslyDisableDeviceAuth=true` (break-glass, severe security downgrade).
|
||||
- All connections must sign the server-provided `connect.challenge` nonce.
|
||||
|
||||
### Device auth migration diagnostics
|
||||
|
||||
For legacy clients that still use pre-challenge signing behavior, `connect` now returns
|
||||
`DEVICE_AUTH_*` detail codes under `error.details.code` with a stable `error.details.reason`.
|
||||
|
||||
Common migration failures:
|
||||
|
||||
| Message | details.code | details.reason | Meaning |
|
||||
| --------------------------- | -------------------------------- | ------------------------ | -------------------------------------------------- |
|
||||
| `device nonce required` | `DEVICE_AUTH_NONCE_REQUIRED` | `device-nonce-missing` | Client omitted `device.nonce` (or sent blank). |
|
||||
| `device nonce mismatch` | `DEVICE_AUTH_NONCE_MISMATCH` | `device-nonce-mismatch` | Client signed with a stale/wrong nonce. |
|
||||
| `device signature invalid` | `DEVICE_AUTH_SIGNATURE_INVALID` | `device-signature` | Signature payload does not match v2 payload. |
|
||||
| `device signature expired` | `DEVICE_AUTH_SIGNATURE_EXPIRED` | `device-signature-stale` | Signed timestamp is outside allowed skew. |
|
||||
| `device identity mismatch` | `DEVICE_AUTH_DEVICE_ID_MISMATCH` | `device-id-mismatch` | `device.id` does not match public key fingerprint. |
|
||||
| `device public key invalid` | `DEVICE_AUTH_PUBLIC_KEY_INVALID` | `device-public-key` | Public key format/canonicalization failed. |
|
||||
|
||||
Migration target:
|
||||
|
||||
- Always wait for `connect.challenge`.
|
||||
- Sign the v2 payload that includes the server nonce.
|
||||
- Send the same nonce in `connect.params.device.nonce`.
|
||||
- Preferred signature payload is `v3`, which binds `platform` and `deviceFamily`
|
||||
in addition to device/client/role/scopes/token/nonce fields.
|
||||
- Legacy `v2` signatures remain accepted for compatibility, but paired-device
|
||||
metadata pinning still controls command policy on reconnect.
|
||||
|
||||
## TLS + pinning
|
||||
|
||||
- TLS is supported for WS connections.
|
||||
- Clients may optionally pin the gateway cert fingerprint (see `gateway.tls`
|
||||
config plus `gateway.remote.tlsFingerprint` or CLI `--tls-fingerprint`).
|
||||
|
||||
## Scope
|
||||
|
||||
This protocol exposes the **full gateway API** (status, channels, models, chat,
|
||||
agent, sessions, nodes, approvals, etc.). The exact surface is defined by the
|
||||
TypeBox schemas in `src/gateway/protocol/schema.ts`.
|
||||
164
openclaw/docs/gateway/remote-gateway-readme.md
Normal file
164
openclaw/docs/gateway/remote-gateway-readme.md
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
---
|
||||
summary: "SSH tunnel setup for OpenClaw.app connecting to a remote gateway"
|
||||
read_when: "Connecting the macOS app to a remote gateway over SSH"
|
||||
title: "Remote Gateway Setup"
|
||||
---
|
||||
|
||||
> This content has been merged into [Remote Access](/gateway/remote#macos-persistent-ssh-tunnel-via-launchagent). See that page for the current guide.
|
||||
|
||||
# Running OpenClaw.app with a Remote Gateway
|
||||
|
||||
OpenClaw.app uses SSH tunneling to connect to a remote gateway. This guide shows you how to set it up.
|
||||
|
||||
## Overview
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Client["Client Machine"]
|
||||
direction TB
|
||||
A["OpenClaw.app"]
|
||||
B["ws://127.0.0.1:18789\n(local port)"]
|
||||
T["SSH Tunnel"]
|
||||
|
||||
A --> B
|
||||
B --> T
|
||||
end
|
||||
subgraph Remote["Remote Machine"]
|
||||
direction TB
|
||||
C["Gateway WebSocket"]
|
||||
D["ws://127.0.0.1:18789"]
|
||||
|
||||
C --> D
|
||||
end
|
||||
T --> C
|
||||
```
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### Step 1: Add SSH Config
|
||||
|
||||
Edit `~/.ssh/config` and add:
|
||||
|
||||
```ssh
|
||||
Host remote-gateway
|
||||
HostName <REMOTE_IP> # e.g., 172.27.187.184
|
||||
User <REMOTE_USER> # e.g., jefferson
|
||||
LocalForward 18789 127.0.0.1:18789
|
||||
IdentityFile ~/.ssh/id_rsa
|
||||
```
|
||||
|
||||
Replace `<REMOTE_IP>` and `<REMOTE_USER>` with your values.
|
||||
|
||||
### Step 2: Copy SSH Key
|
||||
|
||||
Copy your public key to the remote machine (enter password once):
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/id_rsa <REMOTE_USER>@<REMOTE_IP>
|
||||
```
|
||||
|
||||
### Step 3: Configure Remote Gateway Auth
|
||||
|
||||
```bash
|
||||
openclaw config set gateway.remote.token "<your-token>"
|
||||
```
|
||||
|
||||
Use `gateway.remote.password` instead if your remote gateway uses password auth.
|
||||
`OPENCLAW_GATEWAY_TOKEN` is still valid as a shell-level override, but the durable
|
||||
remote-client setup is `gateway.remote.token` / `gateway.remote.password`.
|
||||
|
||||
### Step 4: Start SSH Tunnel
|
||||
|
||||
```bash
|
||||
ssh -N remote-gateway &
|
||||
```
|
||||
|
||||
### Step 5: Restart OpenClaw.app
|
||||
|
||||
```bash
|
||||
# Quit OpenClaw.app (⌘Q), then reopen:
|
||||
open /path/to/OpenClaw.app
|
||||
```
|
||||
|
||||
The app will now connect to the remote gateway through the SSH tunnel.
|
||||
|
||||
---
|
||||
|
||||
## Auto-Start Tunnel on Login
|
||||
|
||||
To have the SSH tunnel start automatically when you log in, create a Launch Agent.
|
||||
|
||||
### Create the PLIST file
|
||||
|
||||
Save this as `~/Library/LaunchAgents/ai.openclaw.ssh-tunnel.plist`:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>ai.openclaw.ssh-tunnel</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/ssh</string>
|
||||
<string>-N</string>
|
||||
<string>remote-gateway</string>
|
||||
</array>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### Load the Launch Agent
|
||||
|
||||
```bash
|
||||
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/ai.openclaw.ssh-tunnel.plist
|
||||
```
|
||||
|
||||
The tunnel will now:
|
||||
|
||||
- Start automatically when you log in
|
||||
- Restart if it crashes
|
||||
- Keep running in the background
|
||||
|
||||
Legacy note: remove any leftover `com.openclaw.ssh-tunnel` LaunchAgent if present.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Check if tunnel is running:**
|
||||
|
||||
```bash
|
||||
ps aux | grep "ssh -N remote-gateway" | grep -v grep
|
||||
lsof -i :18789
|
||||
```
|
||||
|
||||
**Restart the tunnel:**
|
||||
|
||||
```bash
|
||||
launchctl kickstart -k gui/$UID/ai.openclaw.ssh-tunnel
|
||||
```
|
||||
|
||||
**Stop the tunnel:**
|
||||
|
||||
```bash
|
||||
launchctl bootout gui/$UID/ai.openclaw.ssh-tunnel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
| Component | What It Does |
|
||||
| ------------------------------------ | ------------------------------------------------------------ |
|
||||
| `LocalForward 18789 127.0.0.1:18789` | Forwards local port 18789 to remote port 18789 |
|
||||
| `ssh -N` | SSH without executing remote commands (just port forwarding) |
|
||||
| `KeepAlive` | Automatically restarts tunnel if it crashes |
|
||||
| `RunAtLoad` | Starts tunnel when the agent loads |
|
||||
|
||||
OpenClaw.app connects to `ws://127.0.0.1:18789` on your client machine. The SSH tunnel forwards that connection to port 18789 on the remote machine where the Gateway is running.
|
||||
251
openclaw/docs/gateway/remote.md
Normal file
251
openclaw/docs/gateway/remote.md
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
---
|
||||
summary: "Remote access using SSH tunnels (Gateway WS) and tailnets"
|
||||
read_when:
|
||||
- Running or troubleshooting remote gateway setups
|
||||
title: "Remote Access"
|
||||
---
|
||||
|
||||
# Remote access (SSH, tunnels, and tailnets)
|
||||
|
||||
This repo supports “remote over SSH” by keeping a single Gateway (the master) running on a dedicated host (desktop/server) and connecting clients to it.
|
||||
|
||||
- For **operators (you / the macOS app)**: SSH tunneling is the universal fallback.
|
||||
- For **nodes (iOS/Android and future devices)**: connect to the Gateway **WebSocket** (LAN/tailnet or SSH tunnel as needed).
|
||||
|
||||
## The core idea
|
||||
|
||||
- The Gateway WebSocket binds to **loopback** on your configured port (defaults to 18789).
|
||||
- For remote use, you forward that loopback port over SSH (or use a tailnet/VPN and tunnel less).
|
||||
|
||||
## Common VPN/tailnet setups (where the agent lives)
|
||||
|
||||
Think of the **Gateway host** as “where the agent lives.” It owns sessions, auth profiles, channels, and state.
|
||||
Your laptop/desktop (and nodes) connect to that host.
|
||||
|
||||
### 1) Always-on Gateway in your tailnet (VPS or home server)
|
||||
|
||||
Run the Gateway on a persistent host and reach it via **Tailscale** or SSH.
|
||||
|
||||
- **Best UX:** keep `gateway.bind: "loopback"` and use **Tailscale Serve** for the Control UI.
|
||||
- **Fallback:** keep loopback + SSH tunnel from any machine that needs access.
|
||||
- **Examples:** [exe.dev](/install/exe-dev) (easy VM) or [Hetzner](/install/hetzner) (production VPS).
|
||||
|
||||
This is ideal when your laptop sleeps often but you want the agent always-on.
|
||||
|
||||
### 2) Home desktop runs the Gateway, laptop is remote control
|
||||
|
||||
The laptop does **not** run the agent. It connects remotely:
|
||||
|
||||
- Use the macOS app’s **Remote over SSH** mode (Settings → General → “OpenClaw runs”).
|
||||
- The app opens and manages the tunnel, so WebChat + health checks “just work.”
|
||||
|
||||
Runbook: [macOS remote access](/platforms/mac/remote).
|
||||
|
||||
### 3) Laptop runs the Gateway, remote access from other machines
|
||||
|
||||
Keep the Gateway local but expose it safely:
|
||||
|
||||
- SSH tunnel to the laptop from other machines, or
|
||||
- Tailscale Serve the Control UI and keep the Gateway loopback-only.
|
||||
|
||||
Guide: [Tailscale](/gateway/tailscale) and [Web overview](/web).
|
||||
|
||||
## Command flow (what runs where)
|
||||
|
||||
One gateway service owns state + channels. Nodes are peripherals.
|
||||
|
||||
Flow example (Telegram → node):
|
||||
|
||||
- Telegram message arrives at the **Gateway**.
|
||||
- Gateway runs the **agent** and decides whether to call a node tool.
|
||||
- Gateway calls the **node** over the Gateway WebSocket (`node.*` RPC).
|
||||
- Node returns the result; Gateway replies back out to Telegram.
|
||||
|
||||
Notes:
|
||||
|
||||
- **Nodes do not run the gateway service.** Only one gateway should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)).
|
||||
- macOS app “node mode” is just a node client over the Gateway WebSocket.
|
||||
|
||||
## SSH tunnel (CLI + tools)
|
||||
|
||||
Create a local tunnel to the remote Gateway WS:
|
||||
|
||||
```bash
|
||||
ssh -N -L 18789:127.0.0.1:18789 user@host
|
||||
```
|
||||
|
||||
With the tunnel up:
|
||||
|
||||
- `openclaw health` and `openclaw status --deep` now reach the remote gateway via `ws://127.0.0.1:18789`.
|
||||
- `openclaw gateway status`, `openclaw gateway health`, `openclaw gateway probe`, and `openclaw gateway call` can also target the forwarded URL via `--url` when needed.
|
||||
|
||||
Note: replace `18789` with your configured `gateway.port` (or `--port`/`OPENCLAW_GATEWAY_PORT`).
|
||||
Note: when you pass `--url`, the CLI does not fall back to config or environment credentials.
|
||||
Include `--token` or `--password` explicitly. Missing explicit credentials is an error.
|
||||
|
||||
## CLI remote defaults
|
||||
|
||||
You can persist a remote target so CLI commands use it by default:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url: "ws://127.0.0.1:18789",
|
||||
token: "your-token",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
When the gateway is loopback-only, keep the URL at `ws://127.0.0.1:18789` and open the SSH tunnel first.
|
||||
|
||||
## Credential precedence
|
||||
|
||||
Gateway credential resolution follows one shared contract across call/probe/status paths and Discord exec-approval monitoring. Node-host uses the same base contract with one local-mode exception (it intentionally ignores `gateway.remote.*`):
|
||||
|
||||
- Explicit credentials (`--token`, `--password`, or tool `gatewayToken`) always win on call paths that accept explicit auth.
|
||||
- URL override safety:
|
||||
- CLI URL overrides (`--url`) never reuse implicit config/env credentials.
|
||||
- Env URL overrides (`OPENCLAW_GATEWAY_URL`) may use env credentials only (`OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`).
|
||||
- Local mode defaults:
|
||||
- token: `OPENCLAW_GATEWAY_TOKEN` -> `gateway.auth.token` -> `gateway.remote.token` (remote fallback applies only when local auth token input is unset)
|
||||
- password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.auth.password` -> `gateway.remote.password` (remote fallback applies only when local auth password input is unset)
|
||||
- Remote mode defaults:
|
||||
- token: `gateway.remote.token` -> `OPENCLAW_GATEWAY_TOKEN` -> `gateway.auth.token`
|
||||
- password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.remote.password` -> `gateway.auth.password`
|
||||
- Node-host local-mode exception: `gateway.remote.token` / `gateway.remote.password` are ignored.
|
||||
- Remote probe/status token checks are strict by default: they use `gateway.remote.token` only (no local token fallback) when targeting remote mode.
|
||||
- Gateway env overrides use `OPENCLAW_GATEWAY_*` only.
|
||||
|
||||
## Chat UI over SSH
|
||||
|
||||
WebChat no longer uses a separate HTTP port. The SwiftUI chat UI connects directly to the Gateway WebSocket.
|
||||
|
||||
- Forward `18789` over SSH (see above), then connect clients to `ws://127.0.0.1:18789`.
|
||||
- On macOS, prefer the app’s “Remote over SSH” mode, which manages the tunnel automatically.
|
||||
|
||||
## macOS app "Remote over SSH"
|
||||
|
||||
The macOS menu bar app can drive the same setup end-to-end (remote status checks, WebChat, and Voice Wake forwarding).
|
||||
|
||||
Runbook: [macOS remote access](/platforms/mac/remote).
|
||||
|
||||
## Security rules (remote/VPN)
|
||||
|
||||
Short version: **keep the Gateway loopback-only** unless you’re sure you need a bind.
|
||||
|
||||
- **Loopback + SSH/Tailscale Serve** is the safest default (no public exposure).
|
||||
- Plaintext `ws://` is loopback-only by default. For trusted private networks,
|
||||
set `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` on the client process as break-glass.
|
||||
- **Non-loopback binds** (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) must use gateway auth: token, password, or an identity-aware reverse proxy with `gateway.auth.mode: "trusted-proxy"`.
|
||||
- `gateway.remote.token` / `.password` are client credential sources. They do **not** configure server auth by themselves.
|
||||
- Local call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
|
||||
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
|
||||
- `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`.
|
||||
- **Tailscale Serve** can authenticate Control UI/WebSocket traffic via identity
|
||||
headers when `gateway.auth.allowTailscale: true`; HTTP API endpoints do not
|
||||
use that Tailscale header auth and instead follow the gateway's normal HTTP
|
||||
auth mode. This tokenless flow assumes the gateway host is trusted. Set it to
|
||||
`false` if you want shared-secret auth everywhere.
|
||||
- **Trusted-proxy** auth is for non-loopback identity-aware proxy setups only.
|
||||
Same-host loopback reverse proxies do not satisfy `gateway.auth.mode: "trusted-proxy"`.
|
||||
- Treat browser control like operator access: tailnet-only + deliberate node pairing.
|
||||
|
||||
Deep dive: [Security](/gateway/security).
|
||||
|
||||
### macOS: persistent SSH tunnel via LaunchAgent
|
||||
|
||||
For macOS clients connecting to a remote gateway, the easiest persistent setup uses an SSH `LocalForward` config entry plus a LaunchAgent to keep the tunnel alive across reboots and crashes.
|
||||
|
||||
#### Step 1: add SSH config
|
||||
|
||||
Edit `~/.ssh/config`:
|
||||
|
||||
```ssh
|
||||
Host remote-gateway
|
||||
HostName <REMOTE_IP>
|
||||
User <REMOTE_USER>
|
||||
LocalForward 18789 127.0.0.1:18789
|
||||
IdentityFile ~/.ssh/id_rsa
|
||||
```
|
||||
|
||||
Replace `<REMOTE_IP>` and `<REMOTE_USER>` with your values.
|
||||
|
||||
#### Step 2: copy SSH key (one-time)
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/id_rsa <REMOTE_USER>@<REMOTE_IP>
|
||||
```
|
||||
|
||||
#### Step 3: configure the gateway token
|
||||
|
||||
Store the token in config so it persists across restarts:
|
||||
|
||||
```bash
|
||||
openclaw config set gateway.remote.token "<your-token>"
|
||||
```
|
||||
|
||||
#### Step 4: create the LaunchAgent
|
||||
|
||||
Save this as `~/Library/LaunchAgents/ai.openclaw.ssh-tunnel.plist`:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>ai.openclaw.ssh-tunnel</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/ssh</string>
|
||||
<string>-N</string>
|
||||
<string>remote-gateway</string>
|
||||
</array>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
#### Step 5: load the LaunchAgent
|
||||
|
||||
```bash
|
||||
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/ai.openclaw.ssh-tunnel.plist
|
||||
```
|
||||
|
||||
The tunnel will start automatically at login, restart on crash, and keep the forwarded port live.
|
||||
|
||||
Note: if you have a leftover `com.openclaw.ssh-tunnel` LaunchAgent from an older setup, unload and delete it.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
Check if the tunnel is running:
|
||||
|
||||
```bash
|
||||
ps aux | grep "ssh -N remote-gateway" | grep -v grep
|
||||
lsof -i :18789
|
||||
```
|
||||
|
||||
Restart the tunnel:
|
||||
|
||||
```bash
|
||||
launchctl kickstart -k gui/$UID/ai.openclaw.ssh-tunnel
|
||||
```
|
||||
|
||||
Stop the tunnel:
|
||||
|
||||
```bash
|
||||
launchctl bootout gui/$UID/ai.openclaw.ssh-tunnel
|
||||
```
|
||||
|
||||
| Config entry | What it does |
|
||||
| ------------------------------------ | ------------------------------------------------------------ |
|
||||
| `LocalForward 18789 127.0.0.1:18789` | Forwards local port 18789 to remote port 18789 |
|
||||
| `ssh -N` | SSH without executing remote commands (port-forwarding only) |
|
||||
| `KeepAlive` | Automatically restarts the tunnel if it crashes |
|
||||
| `RunAtLoad` | Starts the tunnel when the LaunchAgent loads at login |
|
||||
141
openclaw/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md
Normal file
141
openclaw/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
---
|
||||
title: Sandbox vs Tool Policy vs Elevated
|
||||
summary: "Why a tool is blocked: sandbox runtime, tool allow/deny policy, and elevated exec gates"
|
||||
read_when: "You hit 'sandbox jail' or see a tool/elevated refusal and want the exact config key to change."
|
||||
status: active
|
||||
---
|
||||
|
||||
# Sandbox vs Tool Policy vs Elevated
|
||||
|
||||
OpenClaw has three related (but different) controls:
|
||||
|
||||
1. **Sandbox** (`agents.defaults.sandbox.*` / `agents.list[].sandbox.*`) decides **where tools run** (Docker vs host).
|
||||
2. **Tool policy** (`tools.*`, `tools.sandbox.tools.*`, `agents.list[].tools.*`) decides **which tools are available/allowed**.
|
||||
3. **Elevated** (`tools.elevated.*`, `agents.list[].tools.elevated.*`) is an **exec-only escape hatch** to run outside the sandbox when you’re sandboxed (`gateway` by default, or `node` when the exec target is configured to `node`).
|
||||
|
||||
## Quick debug
|
||||
|
||||
Use the inspector to see what OpenClaw is _actually_ doing:
|
||||
|
||||
```bash
|
||||
openclaw sandbox explain
|
||||
openclaw sandbox explain --session agent:main:main
|
||||
openclaw sandbox explain --agent work
|
||||
openclaw sandbox explain --json
|
||||
```
|
||||
|
||||
It prints:
|
||||
|
||||
- effective sandbox mode/scope/workspace access
|
||||
- whether the session is currently sandboxed (main vs non-main)
|
||||
- effective sandbox tool allow/deny (and whether it came from agent/global/default)
|
||||
- elevated gates and fix-it key paths
|
||||
|
||||
## Sandbox: where tools run
|
||||
|
||||
Sandboxing is controlled by `agents.defaults.sandbox.mode`:
|
||||
|
||||
- `"off"`: everything runs on the host.
|
||||
- `"non-main"`: only non-main sessions are sandboxed (common “surprise” for groups/channels).
|
||||
- `"all"`: everything is sandboxed.
|
||||
|
||||
See [Sandboxing](/gateway/sandboxing) for the full matrix (scope, workspace mounts, images).
|
||||
|
||||
### Bind mounts (security quick check)
|
||||
|
||||
- `docker.binds` _pierces_ the sandbox filesystem: whatever you mount is visible inside the container with the mode you set (`:ro` or `:rw`).
|
||||
- Default is read-write if you omit the mode; prefer `:ro` for source/secrets.
|
||||
- `scope: "shared"` ignores per-agent binds (only global binds apply).
|
||||
- OpenClaw validates bind sources twice: first on the normalized source path, then again after resolving through the deepest existing ancestor. Symlink-parent escapes do not bypass blocked-path or allowed-root checks.
|
||||
- Non-existent leaf paths are still checked safely. If `/workspace/alias-out/new-file` resolves through a symlinked parent to a blocked path or outside the configured allowed roots, the bind is rejected.
|
||||
- Binding `/var/run/docker.sock` effectively hands host control to the sandbox; only do this intentionally.
|
||||
- Workspace access (`workspaceAccess: "ro"`/`"rw"`) is independent of bind modes.
|
||||
|
||||
## Tool policy: which tools exist/are callable
|
||||
|
||||
Two layers matter:
|
||||
|
||||
- **Tool profile**: `tools.profile` and `agents.list[].tools.profile` (base allowlist)
|
||||
- **Provider tool profile**: `tools.byProvider[provider].profile` and `agents.list[].tools.byProvider[provider].profile`
|
||||
- **Global/per-agent tool policy**: `tools.allow`/`tools.deny` and `agents.list[].tools.allow`/`agents.list[].tools.deny`
|
||||
- **Provider tool policy**: `tools.byProvider[provider].allow/deny` and `agents.list[].tools.byProvider[provider].allow/deny`
|
||||
- **Sandbox tool policy** (only applies when sandboxed): `tools.sandbox.tools.allow`/`tools.sandbox.tools.deny` and `agents.list[].tools.sandbox.tools.*`
|
||||
|
||||
Rules of thumb:
|
||||
|
||||
- `deny` always wins.
|
||||
- If `allow` is non-empty, everything else is treated as blocked.
|
||||
- Tool policy is the hard stop: `/exec` cannot override a denied `exec` tool.
|
||||
- `/exec` only changes session defaults for authorized senders; it does not grant tool access.
|
||||
Provider tool keys accept either `provider` (e.g. `google-antigravity`) or `provider/model` (e.g. `openai/gpt-5.4`).
|
||||
|
||||
### Tool groups (shorthands)
|
||||
|
||||
Tool policies (global, agent, sandbox) support `group:*` entries that expand to multiple tools:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
sandbox: {
|
||||
tools: {
|
||||
allow: ["group:runtime", "group:fs", "group:sessions", "group:memory"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Available groups:
|
||||
|
||||
- `group:runtime`: `exec`, `process`, `code_execution` (`bash` is accepted as
|
||||
an alias for `exec`)
|
||||
- `group:fs`: `read`, `write`, `edit`, `apply_patch`
|
||||
- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `sessions_yield`, `subagents`, `session_status`
|
||||
- `group:memory`: `memory_search`, `memory_get`
|
||||
- `group:web`: `web_search`, `x_search`, `web_fetch`
|
||||
- `group:ui`: `browser`, `canvas`
|
||||
- `group:automation`: `cron`, `gateway`
|
||||
- `group:messaging`: `message`
|
||||
- `group:nodes`: `nodes`
|
||||
- `group:agents`: `agents_list`
|
||||
- `group:media`: `image`, `image_generate`, `video_generate`, `tts`
|
||||
- `group:openclaw`: all built-in OpenClaw tools (excludes provider plugins)
|
||||
|
||||
## Elevated: exec-only "run on host"
|
||||
|
||||
Elevated does **not** grant extra tools; it only affects `exec`.
|
||||
|
||||
- If you’re sandboxed, `/elevated on` (or `exec` with `elevated: true`) runs outside the sandbox (approvals may still apply).
|
||||
- Use `/elevated full` to skip exec approvals for the session.
|
||||
- If you’re already running direct, elevated is effectively a no-op (still gated).
|
||||
- Elevated is **not** skill-scoped and does **not** override tool allow/deny.
|
||||
- Elevated does not grant arbitrary cross-host overrides from `host=auto`; it follows the normal exec target rules and only preserves `node` when the configured/session target is already `node`.
|
||||
- `/exec` is separate from elevated. It only adjusts per-session exec defaults for authorized senders.
|
||||
|
||||
Gates:
|
||||
|
||||
- Enablement: `tools.elevated.enabled` (and optionally `agents.list[].tools.elevated.enabled`)
|
||||
- Sender allowlists: `tools.elevated.allowFrom.<provider>` (and optionally `agents.list[].tools.elevated.allowFrom.<provider>`)
|
||||
|
||||
See [Elevated Mode](/tools/elevated).
|
||||
|
||||
## Common "sandbox jail" fixes
|
||||
|
||||
### "Tool X blocked by sandbox tool policy"
|
||||
|
||||
Fix-it keys (pick one):
|
||||
|
||||
- Disable sandbox: `agents.defaults.sandbox.mode=off` (or per-agent `agents.list[].sandbox.mode=off`)
|
||||
- Allow the tool inside sandbox:
|
||||
- remove it from `tools.sandbox.tools.deny` (or per-agent `agents.list[].tools.sandbox.tools.deny`)
|
||||
- or add it to `tools.sandbox.tools.allow` (or per-agent allow)
|
||||
|
||||
### "I thought this was main, why is it sandboxed?"
|
||||
|
||||
In `"non-main"` mode, group/channel keys are _not_ main. Use the main session key (shown by `sandbox explain`) or switch mode to `"off"`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Sandboxing](/gateway/sandboxing) -- full sandbox reference (modes, scopes, backends, images)
|
||||
- [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) -- per-agent overrides and precedence
|
||||
- [Elevated Mode](/tools/elevated)
|
||||
485
openclaw/docs/gateway/sandboxing.md
Normal file
485
openclaw/docs/gateway/sandboxing.md
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
---
|
||||
summary: "How OpenClaw sandboxing works: modes, scopes, workspace access, and images"
|
||||
title: Sandboxing
|
||||
read_when: "You want a dedicated explanation of sandboxing or need to tune agents.defaults.sandbox."
|
||||
status: active
|
||||
---
|
||||
|
||||
# Sandboxing
|
||||
|
||||
OpenClaw can run **tools inside sandbox backends** to reduce blast radius.
|
||||
This is **optional** and controlled by configuration (`agents.defaults.sandbox` or
|
||||
`agents.list[].sandbox`). If sandboxing is off, tools run on the host.
|
||||
The Gateway stays on the host; tool execution runs in an isolated sandbox
|
||||
when enabled.
|
||||
|
||||
This is not a perfect security boundary, but it materially limits filesystem
|
||||
and process access when the model does something dumb.
|
||||
|
||||
## What gets sandboxed
|
||||
|
||||
- Tool execution (`exec`, `read`, `write`, `edit`, `apply_patch`, `process`, etc.).
|
||||
- Optional sandboxed browser (`agents.defaults.sandbox.browser`).
|
||||
- By default, the sandbox browser auto-starts (ensures CDP is reachable) when the browser tool needs it.
|
||||
Configure via `agents.defaults.sandbox.browser.autoStart` and `agents.defaults.sandbox.browser.autoStartTimeoutMs`.
|
||||
- By default, sandbox browser containers use a dedicated Docker network (`openclaw-sandbox-browser`) instead of the global `bridge` network.
|
||||
Configure with `agents.defaults.sandbox.browser.network`.
|
||||
- Optional `agents.defaults.sandbox.browser.cdpSourceRange` restricts container-edge CDP ingress with a CIDR allowlist (for example `172.21.0.1/32`).
|
||||
- noVNC observer access is password-protected by default; OpenClaw emits a short-lived token URL that serves a local bootstrap page and opens noVNC with password in URL fragment (not query/header logs).
|
||||
- `agents.defaults.sandbox.browser.allowHostControl` lets sandboxed sessions target the host browser explicitly.
|
||||
- Optional allowlists gate `target: "custom"`: `allowedControlUrls`, `allowedControlHosts`, `allowedControlPorts`.
|
||||
|
||||
Not sandboxed:
|
||||
|
||||
- The Gateway process itself.
|
||||
- Any tool explicitly allowed to run outside the sandbox (e.g. `tools.elevated`).
|
||||
- **Elevated exec bypasses sandboxing and uses the configured escape path (`gateway` by default, or `node` when the exec target is `node`).**
|
||||
- If sandboxing is off, `tools.elevated` does not change execution (already on host). See [Elevated Mode](/tools/elevated).
|
||||
|
||||
## Modes
|
||||
|
||||
`agents.defaults.sandbox.mode` controls **when** sandboxing is used:
|
||||
|
||||
- `"off"`: no sandboxing.
|
||||
- `"non-main"`: sandbox only **non-main** sessions (default if you want normal chats on host).
|
||||
- `"all"`: every session runs in a sandbox.
|
||||
Note: `"non-main"` is based on `session.mainKey` (default `"main"`), not agent id.
|
||||
Group/channel sessions use their own keys, so they count as non-main and will be sandboxed.
|
||||
|
||||
## Scope
|
||||
|
||||
`agents.defaults.sandbox.scope` controls **how many containers** are created:
|
||||
|
||||
- `"agent"` (default): one container per agent.
|
||||
- `"session"`: one container per session.
|
||||
- `"shared"`: one container shared by all sandboxed sessions.
|
||||
|
||||
## Backend
|
||||
|
||||
`agents.defaults.sandbox.backend` controls **which runtime** provides the sandbox:
|
||||
|
||||
- `"docker"` (default): local Docker-backed sandbox runtime.
|
||||
- `"ssh"`: generic SSH-backed remote sandbox runtime.
|
||||
- `"openshell"`: OpenShell-backed sandbox runtime.
|
||||
|
||||
SSH-specific config lives under `agents.defaults.sandbox.ssh`.
|
||||
OpenShell-specific config lives under `plugins.entries.openshell.config`.
|
||||
|
||||
### Choosing a backend
|
||||
|
||||
| | Docker | SSH | OpenShell |
|
||||
| ------------------- | -------------------------------- | ------------------------------ | --------------------------------------------------- |
|
||||
| **Where it runs** | Local container | Any SSH-accessible host | OpenShell managed sandbox |
|
||||
| **Setup** | `scripts/sandbox-setup.sh` | SSH key + target host | OpenShell plugin enabled |
|
||||
| **Workspace model** | Bind-mount or copy | Remote-canonical (seed once) | `mirror` or `remote` |
|
||||
| **Network control** | `docker.network` (default: none) | Depends on remote host | Depends on OpenShell |
|
||||
| **Browser sandbox** | Supported | Not supported | Not supported yet |
|
||||
| **Bind mounts** | `docker.binds` | N/A | N/A |
|
||||
| **Best for** | Local dev, full isolation | Offloading to a remote machine | Managed remote sandboxes with optional two-way sync |
|
||||
|
||||
### Docker backend
|
||||
|
||||
The Docker backend is the default runtime, executing tools and sandbox browsers locally via the Docker daemon socket (`/var/run/docker.sock`). Sandbox container isolation is determined by Docker namespaces.
|
||||
|
||||
**Docker-out-of-Docker (DooD) Constraints**:
|
||||
If you deploy the OpenClaw Gateway itself as a Docker container, it orchestrates sibling sandbox containers using the host's Docker socket (DooD). This introduces a specific path mapping constraint:
|
||||
|
||||
- **Config Requires Host Paths**: The `openclaw.json` `workspace` configuration MUST contain the **Host's absolute path** (e.g. `/home/user/.openclaw/workspaces`), not the internal Gateway container path. When OpenClaw asks the Docker daemon to spawn a sandbox, the daemon evaluates paths relative to the Host OS namespace, not the Gateway namespace.
|
||||
- **FS Bridge Parity (Identical Volume Map)**: The OpenClaw Gateway native process also writes heartbeat and bridge files to the `workspace` directory. Because the Gateway evaluates the exact same string (the host path) from within its own containerized environment, the Gateway deployment MUST include an identical volume map linking the host namespace natively (`-v /home/user/.openclaw:/home/user/.openclaw`).
|
||||
|
||||
If you map paths internally without absolute host parity, OpenClaw natively throws an `EACCES` permission error attempting to write its heartbeat inside the container environment because the fully qualified path string doesn't exist natively.
|
||||
|
||||
### SSH backend
|
||||
|
||||
Use `backend: "ssh"` when you want OpenClaw to sandbox `exec`, file tools, and media reads on
|
||||
an arbitrary SSH-accessible machine.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "ssh",
|
||||
scope: "session",
|
||||
workspaceAccess: "rw",
|
||||
ssh: {
|
||||
target: "user@gateway-host:22",
|
||||
workspaceRoot: "/tmp/openclaw-sandboxes",
|
||||
strictHostKeyChecking: true,
|
||||
updateHostKeys: true,
|
||||
identityFile: "~/.ssh/id_ed25519",
|
||||
certificateFile: "~/.ssh/id_ed25519-cert.pub",
|
||||
knownHostsFile: "~/.ssh/known_hosts",
|
||||
// Or use SecretRefs / inline contents instead of local files:
|
||||
// identityData: { source: "env", provider: "default", id: "SSH_IDENTITY" },
|
||||
// certificateData: { source: "env", provider: "default", id: "SSH_CERTIFICATE" },
|
||||
// knownHostsData: { source: "env", provider: "default", id: "SSH_KNOWN_HOSTS" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
How it works:
|
||||
|
||||
- OpenClaw creates a per-scope remote root under `sandbox.ssh.workspaceRoot`.
|
||||
- On first use after create or recreate, OpenClaw seeds that remote workspace from the local workspace once.
|
||||
- After that, `exec`, `read`, `write`, `edit`, `apply_patch`, prompt media reads, and inbound media staging run directly against the remote workspace over SSH.
|
||||
- OpenClaw does not sync remote changes back to the local workspace automatically.
|
||||
|
||||
Authentication material:
|
||||
|
||||
- `identityFile`, `certificateFile`, `knownHostsFile`: use existing local files and pass them through OpenSSH config.
|
||||
- `identityData`, `certificateData`, `knownHostsData`: use inline strings or SecretRefs. OpenClaw resolves them through the normal secrets runtime snapshot, writes them to temp files with `0600`, and deletes them when the SSH session ends.
|
||||
- If both `*File` and `*Data` are set for the same item, `*Data` wins for that SSH session.
|
||||
|
||||
This is a **remote-canonical** model. The remote SSH workspace becomes the real sandbox state after the initial seed.
|
||||
|
||||
Important consequences:
|
||||
|
||||
- Host-local edits made outside OpenClaw after the seed step are not visible remotely until you recreate the sandbox.
|
||||
- `openclaw sandbox recreate` deletes the per-scope remote root and seeds again from local on next use.
|
||||
- Browser sandboxing is not supported on the SSH backend.
|
||||
- `sandbox.docker.*` settings do not apply to the SSH backend.
|
||||
|
||||
### OpenShell backend
|
||||
|
||||
Use `backend: "openshell"` when you want OpenClaw to sandbox tools in an
|
||||
OpenShell-managed remote environment. For the full setup guide, configuration
|
||||
reference, and workspace mode comparison, see the dedicated
|
||||
[OpenShell page](/gateway/openshell).
|
||||
|
||||
OpenShell reuses the same core SSH transport and remote filesystem bridge as the
|
||||
generic SSH backend, and adds OpenShell-specific lifecycle
|
||||
(`sandbox create/get/delete`, `sandbox ssh-config`) plus the optional `mirror`
|
||||
workspace mode.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "session",
|
||||
workspaceAccess: "rw",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
openshell: {
|
||||
enabled: true,
|
||||
config: {
|
||||
from: "openclaw",
|
||||
mode: "remote", // mirror | remote
|
||||
remoteWorkspaceDir: "/sandbox",
|
||||
remoteAgentWorkspaceDir: "/agent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
OpenShell modes:
|
||||
|
||||
- `mirror` (default): local workspace stays canonical. OpenClaw syncs local files into OpenShell before exec and syncs the remote workspace back after exec.
|
||||
- `remote`: OpenShell workspace is canonical after the sandbox is created. OpenClaw seeds the remote workspace once from the local workspace, then file tools and exec run directly against the remote sandbox without syncing changes back.
|
||||
|
||||
Remote transport details:
|
||||
|
||||
- OpenClaw asks OpenShell for sandbox-specific SSH config via `openshell sandbox ssh-config <name>`.
|
||||
- Core writes that SSH config to a temp file, opens the SSH session, and reuses the same remote filesystem bridge used by `backend: "ssh"`.
|
||||
- In `mirror` mode only the lifecycle differs: sync local to remote before exec, then sync back after exec.
|
||||
|
||||
Current OpenShell limitations:
|
||||
|
||||
- sandbox browser is not supported yet
|
||||
- `sandbox.docker.binds` is not supported on the OpenShell backend
|
||||
- Docker-specific runtime knobs under `sandbox.docker.*` still apply only to the Docker backend
|
||||
|
||||
#### Workspace modes
|
||||
|
||||
OpenShell has two workspace models. This is the part that matters most in practice.
|
||||
|
||||
##### `mirror`
|
||||
|
||||
Use `plugins.entries.openshell.config.mode: "mirror"` when you want the **local workspace to stay canonical**.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Before `exec`, OpenClaw syncs the local workspace into the OpenShell sandbox.
|
||||
- After `exec`, OpenClaw syncs the remote workspace back to the local workspace.
|
||||
- File tools still operate through the sandbox bridge, but the local workspace remains the source of truth between turns.
|
||||
|
||||
Use this when:
|
||||
|
||||
- you edit files locally outside OpenClaw and want those changes to show up in the sandbox automatically
|
||||
- you want the OpenShell sandbox to behave as much like the Docker backend as possible
|
||||
- you want the host workspace to reflect sandbox writes after each exec turn
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- extra sync cost before and after exec
|
||||
|
||||
##### `remote`
|
||||
|
||||
Use `plugins.entries.openshell.config.mode: "remote"` when you want the **OpenShell workspace to become canonical**.
|
||||
|
||||
Behavior:
|
||||
|
||||
- When the sandbox is first created, OpenClaw seeds the remote workspace from the local workspace once.
|
||||
- After that, `exec`, `read`, `write`, `edit`, and `apply_patch` operate directly against the remote OpenShell workspace.
|
||||
- OpenClaw does **not** sync remote changes back into the local workspace after exec.
|
||||
- Prompt-time media reads still work because file and media tools read through the sandbox bridge instead of assuming a local host path.
|
||||
- Transport is SSH into the OpenShell sandbox returned by `openshell sandbox ssh-config`.
|
||||
|
||||
Important consequences:
|
||||
|
||||
- If you edit files on the host outside OpenClaw after the seed step, the remote sandbox will **not** see those changes automatically.
|
||||
- If the sandbox is recreated, the remote workspace is seeded from the local workspace again.
|
||||
- With `scope: "agent"` or `scope: "shared"`, that remote workspace is shared at that same scope.
|
||||
|
||||
Use this when:
|
||||
|
||||
- the sandbox should live primarily on the remote OpenShell side
|
||||
- you want lower per-turn sync overhead
|
||||
- you do not want host-local edits to silently overwrite remote sandbox state
|
||||
|
||||
Choose `mirror` if you think of the sandbox as a temporary execution environment.
|
||||
Choose `remote` if you think of the sandbox as the real workspace.
|
||||
|
||||
#### OpenShell lifecycle
|
||||
|
||||
OpenShell sandboxes are still managed through the normal sandbox lifecycle:
|
||||
|
||||
- `openclaw sandbox list` shows OpenShell runtimes as well as Docker runtimes
|
||||
- `openclaw sandbox recreate` deletes the current runtime and lets OpenClaw recreate it on next use
|
||||
- prune logic is backend-aware too
|
||||
|
||||
For `remote` mode, recreate is especially important:
|
||||
|
||||
- recreate deletes the canonical remote workspace for that scope
|
||||
- the next use seeds a fresh remote workspace from the local workspace
|
||||
|
||||
For `mirror` mode, recreate mainly resets the remote execution environment
|
||||
because the local workspace remains canonical anyway.
|
||||
|
||||
## Workspace access
|
||||
|
||||
`agents.defaults.sandbox.workspaceAccess` controls **what the sandbox can see**:
|
||||
|
||||
- `"none"` (default): tools see a sandbox workspace under `~/.openclaw/sandboxes`.
|
||||
- `"ro"`: mounts the agent workspace read-only at `/agent` (disables `write`/`edit`/`apply_patch`).
|
||||
- `"rw"`: mounts the agent workspace read/write at `/workspace`.
|
||||
|
||||
With the OpenShell backend:
|
||||
|
||||
- `mirror` mode still uses the local workspace as the canonical source between exec turns
|
||||
- `remote` mode uses the remote OpenShell workspace as the canonical source after the initial seed
|
||||
- `workspaceAccess: "ro"` and `"none"` still restrict write behavior the same way
|
||||
|
||||
Inbound media is copied into the active sandbox workspace (`media/inbound/*`).
|
||||
Skills note: the `read` tool is sandbox-rooted. With `workspaceAccess: "none"`,
|
||||
OpenClaw mirrors eligible skills into the sandbox workspace (`.../skills`) so
|
||||
they can be read. With `"rw"`, workspace skills are readable from
|
||||
`/workspace/skills`.
|
||||
|
||||
## Custom bind mounts
|
||||
|
||||
`agents.defaults.sandbox.docker.binds` mounts additional host directories into the container.
|
||||
Format: `host:container:mode` (e.g., `"/home/user/source:/source:rw"`).
|
||||
|
||||
Global and per-agent binds are **merged** (not replaced). Under `scope: "shared"`, per-agent binds are ignored.
|
||||
|
||||
`agents.defaults.sandbox.browser.binds` mounts additional host directories into the **sandbox browser** container only.
|
||||
|
||||
- When set (including `[]`), it replaces `agents.defaults.sandbox.docker.binds` for the browser container.
|
||||
- When omitted, the browser container falls back to `agents.defaults.sandbox.docker.binds` (backwards compatible).
|
||||
|
||||
Example (read-only source + an extra data directory):
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
docker: {
|
||||
binds: ["/home/user/source:/source:ro", "/var/data/myapp:/data:ro"],
|
||||
},
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "build",
|
||||
sandbox: {
|
||||
docker: {
|
||||
binds: ["/mnt/cache:/cache:rw"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Security notes:
|
||||
|
||||
- Binds bypass the sandbox filesystem: they expose host paths with whatever mode you set (`:ro` or `:rw`).
|
||||
- OpenClaw blocks dangerous bind sources (for example: `docker.sock`, `/etc`, `/proc`, `/sys`, `/dev`, and parent mounts that would expose them).
|
||||
- OpenClaw also blocks common home-directory credential roots such as `~/.aws`, `~/.cargo`, `~/.config`, `~/.docker`, `~/.gnupg`, `~/.netrc`, `~/.npm`, and `~/.ssh`.
|
||||
- Bind validation is not just string matching. OpenClaw normalizes the source path, then resolves it again through the deepest existing ancestor before re-checking blocked paths and allowed roots.
|
||||
- That means symlink-parent escapes still fail closed even when the final leaf does not exist yet. Example: `/workspace/run-link/new-file` still resolves as `/var/run/...` if `run-link` points there.
|
||||
- Allowed source roots are canonicalized the same way, so a path that only looks inside the allowlist before symlink resolution is still rejected as `outside allowed roots`.
|
||||
- Sensitive mounts (secrets, SSH keys, service credentials) should be `:ro` unless absolutely required.
|
||||
- Combine with `workspaceAccess: "ro"` if you only need read access to the workspace; bind modes stay independent.
|
||||
- See [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) for how binds interact with tool policy and elevated exec.
|
||||
|
||||
## Images + setup
|
||||
|
||||
Default Docker image: `openclaw-sandbox:bookworm-slim`
|
||||
|
||||
Build it once:
|
||||
|
||||
```bash
|
||||
scripts/sandbox-setup.sh
|
||||
```
|
||||
|
||||
Note: the default image does **not** include Node. If a skill needs Node (or
|
||||
other runtimes), either bake a custom image or install via
|
||||
`sandbox.docker.setupCommand` (requires network egress + writable root +
|
||||
root user).
|
||||
|
||||
If you want a more functional sandbox image with common tooling (for example
|
||||
`curl`, `jq`, `nodejs`, `python3`, `git`), build:
|
||||
|
||||
```bash
|
||||
scripts/sandbox-common-setup.sh
|
||||
```
|
||||
|
||||
Then set `agents.defaults.sandbox.docker.image` to
|
||||
`openclaw-sandbox-common:bookworm-slim`.
|
||||
|
||||
Sandboxed browser image:
|
||||
|
||||
```bash
|
||||
scripts/sandbox-browser-setup.sh
|
||||
```
|
||||
|
||||
By default, Docker sandbox containers run with **no network**.
|
||||
Override with `agents.defaults.sandbox.docker.network`.
|
||||
|
||||
The bundled sandbox browser image also applies conservative Chromium startup defaults
|
||||
for containerized workloads. Current container defaults include:
|
||||
|
||||
- `--remote-debugging-address=127.0.0.1`
|
||||
- `--remote-debugging-port=<derived from OPENCLAW_BROWSER_CDP_PORT>`
|
||||
- `--user-data-dir=${HOME}/.chrome`
|
||||
- `--no-first-run`
|
||||
- `--no-default-browser-check`
|
||||
- `--disable-3d-apis`
|
||||
- `--disable-gpu`
|
||||
- `--disable-dev-shm-usage`
|
||||
- `--disable-background-networking`
|
||||
- `--disable-extensions`
|
||||
- `--disable-features=TranslateUI`
|
||||
- `--disable-breakpad`
|
||||
- `--disable-crash-reporter`
|
||||
- `--disable-software-rasterizer`
|
||||
- `--no-zygote`
|
||||
- `--metrics-recording-only`
|
||||
- `--renderer-process-limit=2`
|
||||
- `--no-sandbox` and `--disable-setuid-sandbox` when `noSandbox` is enabled.
|
||||
- The three graphics hardening flags (`--disable-3d-apis`,
|
||||
`--disable-software-rasterizer`, `--disable-gpu`) are optional and are useful
|
||||
when containers lack GPU support. Set `OPENCLAW_BROWSER_DISABLE_GRAPHICS_FLAGS=0`
|
||||
if your workload requires WebGL or other 3D/browser features.
|
||||
- `--disable-extensions` is enabled by default and can be disabled with
|
||||
`OPENCLAW_BROWSER_DISABLE_EXTENSIONS=0` for extension-reliant flows.
|
||||
- `--renderer-process-limit=2` is controlled by
|
||||
`OPENCLAW_BROWSER_RENDERER_PROCESS_LIMIT=<N>`, where `0` keeps Chromium's default.
|
||||
|
||||
If you need a different runtime profile, use a custom browser image and provide
|
||||
your own entrypoint. For local (non-container) Chromium profiles, use
|
||||
`browser.extraArgs` to append additional startup flags.
|
||||
|
||||
Security defaults:
|
||||
|
||||
- `network: "host"` is blocked.
|
||||
- `network: "container:<id>"` is blocked by default (namespace join bypass risk).
|
||||
- Break-glass override: `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true`.
|
||||
|
||||
Docker installs and the containerized gateway live here:
|
||||
[Docker](/install/docker)
|
||||
|
||||
For Docker gateway deployments, `scripts/docker/setup.sh` can bootstrap sandbox config.
|
||||
Set `OPENCLAW_SANDBOX=1` (or `true`/`yes`/`on`) to enable that path. You can
|
||||
override socket location with `OPENCLAW_DOCKER_SOCKET`. Full setup and env
|
||||
reference: [Docker](/install/docker#agent-sandbox).
|
||||
|
||||
## setupCommand (one-time container setup)
|
||||
|
||||
`setupCommand` runs **once** after the sandbox container is created (not on every run).
|
||||
It executes inside the container via `sh -lc`.
|
||||
|
||||
Paths:
|
||||
|
||||
- Global: `agents.defaults.sandbox.docker.setupCommand`
|
||||
- Per-agent: `agents.list[].sandbox.docker.setupCommand`
|
||||
|
||||
Common pitfalls:
|
||||
|
||||
- Default `docker.network` is `"none"` (no egress), so package installs will fail.
|
||||
- `docker.network: "container:<id>"` requires `dangerouslyAllowContainerNamespaceJoin: true` and is break-glass only.
|
||||
- `readOnlyRoot: true` prevents writes; set `readOnlyRoot: false` or bake a custom image.
|
||||
- `user` must be root for package installs (omit `user` or set `user: "0:0"`).
|
||||
- Sandbox exec does **not** inherit host `process.env`. Use
|
||||
`agents.defaults.sandbox.docker.env` (or a custom image) for skill API keys.
|
||||
|
||||
## Tool policy + escape hatches
|
||||
|
||||
Tool allow/deny policies still apply before sandbox rules. If a tool is denied
|
||||
globally or per-agent, sandboxing doesn’t bring it back.
|
||||
|
||||
`tools.elevated` is an explicit escape hatch that runs `exec` outside the sandbox (`gateway` by default, or `node` when the exec target is `node`).
|
||||
`/exec` directives only apply for authorized senders and persist per session; to hard-disable
|
||||
`exec`, use tool policy deny (see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated)).
|
||||
|
||||
Debugging:
|
||||
|
||||
- Use `openclaw sandbox explain` to inspect effective sandbox mode, tool policy, and fix-it config keys.
|
||||
- See [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) for the “why is this blocked?” mental model.
|
||||
Keep it locked down.
|
||||
|
||||
## Multi-agent overrides
|
||||
|
||||
Each agent can override sandbox + tools:
|
||||
`agents.list[].sandbox` and `agents.list[].tools` (plus `agents.list[].tools.sandbox.tools` for sandbox tool policy).
|
||||
See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for precedence.
|
||||
|
||||
## Minimal enable example
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
scope: "session",
|
||||
workspaceAccess: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Related docs
|
||||
|
||||
- [OpenShell](/gateway/openshell) -- managed sandbox backend setup, workspace modes, and config reference
|
||||
- [Sandbox Configuration](/gateway/configuration-reference#agentsdefaultssandbox)
|
||||
- [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) -- debugging "why is this blocked?"
|
||||
- [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) -- per-agent overrides and precedence
|
||||
- [Security](/gateway/security)
|
||||
116
openclaw/docs/gateway/secrets-plan-contract.md
Normal file
116
openclaw/docs/gateway/secrets-plan-contract.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
summary: "Contract for `secrets apply` plans: target validation, path matching, and `auth-profiles.json` target scope"
|
||||
read_when:
|
||||
- Generating or reviewing `openclaw secrets apply` plans
|
||||
- Debugging `Invalid plan target path` errors
|
||||
- Understanding target type and path validation behavior
|
||||
title: "Secrets Apply Plan Contract"
|
||||
---
|
||||
|
||||
# Secrets apply plan contract
|
||||
|
||||
This page defines the strict contract enforced by `openclaw secrets apply`.
|
||||
|
||||
If a target does not match these rules, apply fails before mutating configuration.
|
||||
|
||||
## Plan file shape
|
||||
|
||||
`openclaw secrets apply --from <plan.json>` expects a `targets` array of plan targets:
|
||||
|
||||
```json5
|
||||
{
|
||||
version: 1,
|
||||
protocolVersion: 1,
|
||||
targets: [
|
||||
{
|
||||
type: "models.providers.apiKey",
|
||||
path: "models.providers.openai.apiKey",
|
||||
pathSegments: ["models", "providers", "openai", "apiKey"],
|
||||
providerId: "openai",
|
||||
ref: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
},
|
||||
{
|
||||
type: "auth-profiles.api_key.key",
|
||||
path: "profiles.openai:default.key",
|
||||
pathSegments: ["profiles", "openai:default", "key"],
|
||||
agentId: "main",
|
||||
ref: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Supported target scope
|
||||
|
||||
Plan targets are accepted for supported credential paths in:
|
||||
|
||||
- [SecretRef Credential Surface](/reference/secretref-credential-surface)
|
||||
|
||||
## Target type behavior
|
||||
|
||||
General rule:
|
||||
|
||||
- `target.type` must be recognized and must match the normalized `target.path` shape.
|
||||
|
||||
Compatibility aliases remain accepted for existing plans:
|
||||
|
||||
- `models.providers.apiKey`
|
||||
- `skills.entries.apiKey`
|
||||
- `channels.googlechat.serviceAccount`
|
||||
|
||||
## Path validation rules
|
||||
|
||||
Each target is validated with all of the following:
|
||||
|
||||
- `type` must be a recognized target type.
|
||||
- `path` must be a non-empty dot path.
|
||||
- `pathSegments` can be omitted. If provided, it must normalize to exactly the same path as `path`.
|
||||
- Forbidden segments are rejected: `__proto__`, `prototype`, `constructor`.
|
||||
- The normalized path must match the registered path shape for the target type.
|
||||
- If `providerId` or `accountId` is set, it must match the id encoded in the path.
|
||||
- `auth-profiles.json` targets require `agentId`.
|
||||
- When creating a new `auth-profiles.json` mapping, include `authProfileProvider`.
|
||||
|
||||
## Failure behavior
|
||||
|
||||
If a target fails validation, apply exits with an error like:
|
||||
|
||||
```text
|
||||
Invalid plan target path for models.providers.apiKey: models.providers.openai.baseUrl
|
||||
```
|
||||
|
||||
No writes are committed for an invalid plan.
|
||||
|
||||
## Exec provider consent behavior
|
||||
|
||||
- `--dry-run` skips exec SecretRef checks by default.
|
||||
- Plans containing exec SecretRefs/providers are rejected in write mode unless `--allow-exec` is set.
|
||||
- When validating/applying exec-containing plans, pass `--allow-exec` in both dry-run and write commands.
|
||||
|
||||
## Runtime and audit scope notes
|
||||
|
||||
- Ref-only `auth-profiles.json` entries (`keyRef`/`tokenRef`) are included in runtime resolution and audit coverage.
|
||||
- `secrets apply` writes supported `openclaw.json` targets, supported `auth-profiles.json` targets, and optional scrub targets.
|
||||
|
||||
## Operator checks
|
||||
|
||||
```bash
|
||||
# Validate plan without writes
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run
|
||||
|
||||
# Then apply for real
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json
|
||||
|
||||
# For exec-containing plans, opt in explicitly in both modes
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run --allow-exec
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --allow-exec
|
||||
```
|
||||
|
||||
If apply fails with an invalid target path message, regenerate the plan with `openclaw secrets configure` or fix the target path to a supported shape above.
|
||||
|
||||
## Related docs
|
||||
|
||||
- [Secrets Management](/gateway/secrets)
|
||||
- [CLI `secrets`](/cli/secrets)
|
||||
- [SecretRef Credential Surface](/reference/secretref-credential-surface)
|
||||
- [Configuration Reference](/gateway/configuration-reference)
|
||||
541
openclaw/docs/gateway/secrets.md
Normal file
541
openclaw/docs/gateway/secrets.md
Normal file
|
|
@ -0,0 +1,541 @@
|
|||
---
|
||||
summary: "Secrets management: SecretRef contract, runtime snapshot behavior, and safe one-way scrubbing"
|
||||
read_when:
|
||||
- Configuring SecretRefs for provider credentials and `auth-profiles.json` refs
|
||||
- Operating secrets reload, audit, configure, and apply safely in production
|
||||
- Understanding startup fail-fast, inactive-surface filtering, and last-known-good behavior
|
||||
title: "Secrets Management"
|
||||
---
|
||||
|
||||
# Secrets management
|
||||
|
||||
OpenClaw supports additive SecretRefs so supported credentials do not need to be stored as plaintext in configuration.
|
||||
|
||||
Plaintext still works. SecretRefs are opt-in per credential.
|
||||
|
||||
## Goals and runtime model
|
||||
|
||||
Secrets are resolved into an in-memory runtime snapshot.
|
||||
|
||||
- Resolution is eager during activation, not lazy on request paths.
|
||||
- Startup fails fast when an effectively active SecretRef cannot be resolved.
|
||||
- Reload uses atomic swap: full success, or keep the last-known-good snapshot.
|
||||
- SecretRef policy violations (for example OAuth-mode auth profiles combined with SecretRef input) fail activation before runtime swap.
|
||||
- Runtime requests read from the active in-memory snapshot only.
|
||||
- After the first successful config activation/load, runtime code paths keep reading that active in-memory snapshot until a successful reload swaps it.
|
||||
- Outbound delivery paths also read from that active snapshot (for example Discord reply/thread delivery and Telegram action sends); they do not re-resolve SecretRefs on each send.
|
||||
|
||||
This keeps secret-provider outages off hot request paths.
|
||||
|
||||
## Active-surface filtering
|
||||
|
||||
SecretRefs are validated only on effectively active surfaces.
|
||||
|
||||
- Enabled surfaces: unresolved refs block startup/reload.
|
||||
- Inactive surfaces: unresolved refs do not block startup/reload.
|
||||
- Inactive refs emit non-fatal diagnostics with code `SECRETS_REF_IGNORED_INACTIVE_SURFACE`.
|
||||
|
||||
Examples of inactive surfaces:
|
||||
|
||||
- Disabled channel/account entries.
|
||||
- Top-level channel credentials that no enabled account inherits.
|
||||
- Disabled tool/feature surfaces.
|
||||
- Web search provider-specific keys that are not selected by `tools.web.search.provider`.
|
||||
In auto mode (provider unset), keys are consulted by precedence for provider auto-detection until one resolves.
|
||||
After selection, non-selected provider keys are treated as inactive until selected.
|
||||
- Sandbox SSH auth material (`agents.defaults.sandbox.ssh.identityData`,
|
||||
`certificateData`, `knownHostsData`, plus per-agent overrides) is active only
|
||||
when the effective sandbox backend is `ssh` for the default agent or an enabled agent.
|
||||
- `gateway.remote.token` / `gateway.remote.password` SecretRefs are active if one of these is true:
|
||||
- `gateway.mode=remote`
|
||||
- `gateway.remote.url` is configured
|
||||
- `gateway.tailscale.mode` is `serve` or `funnel`
|
||||
- In local mode without those remote surfaces:
|
||||
- `gateway.remote.token` is active when token auth can win and no env/auth token is configured.
|
||||
- `gateway.remote.password` is active only when password auth can win and no env/auth password is configured.
|
||||
- `gateway.auth.token` SecretRef is inactive for startup auth resolution when `OPENCLAW_GATEWAY_TOKEN` is set, because env token input wins for that runtime.
|
||||
|
||||
## Gateway auth surface diagnostics
|
||||
|
||||
When a SecretRef is configured on `gateway.auth.token`, `gateway.auth.password`,
|
||||
`gateway.remote.token`, or `gateway.remote.password`, gateway startup/reload logs the
|
||||
surface state explicitly:
|
||||
|
||||
- `active`: the SecretRef is part of the effective auth surface and must resolve.
|
||||
- `inactive`: the SecretRef is ignored for this runtime because another auth surface wins, or
|
||||
because remote auth is disabled/not active.
|
||||
|
||||
These entries are logged with `SECRETS_GATEWAY_AUTH_SURFACE` and include the reason used by the
|
||||
active-surface policy, so you can see why a credential was treated as active or inactive.
|
||||
|
||||
## Onboarding reference preflight
|
||||
|
||||
When onboarding runs in interactive mode and you choose SecretRef storage, OpenClaw runs preflight validation before saving:
|
||||
|
||||
- Env refs: validates env var name and confirms a non-empty value is visible during setup.
|
||||
- Provider refs (`file` or `exec`): validates provider selection, resolves `id`, and checks resolved value type.
|
||||
- Quickstart reuse path: when `gateway.auth.token` is already a SecretRef, onboarding resolves it before probe/dashboard bootstrap (for `env`, `file`, and `exec` refs) using the same fail-fast gate.
|
||||
|
||||
If validation fails, onboarding shows the error and lets you retry.
|
||||
|
||||
## SecretRef contract
|
||||
|
||||
Use one object shape everywhere:
|
||||
|
||||
```json5
|
||||
{ source: "env" | "file" | "exec", provider: "default", id: "..." }
|
||||
```
|
||||
|
||||
### `source: "env"`
|
||||
|
||||
```json5
|
||||
{ source: "env", provider: "default", id: "OPENAI_API_KEY" }
|
||||
```
|
||||
|
||||
Validation:
|
||||
|
||||
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
|
||||
- `id` must match `^[A-Z][A-Z0-9_]{0,127}$`
|
||||
|
||||
### `source: "file"`
|
||||
|
||||
```json5
|
||||
{ source: "file", provider: "filemain", id: "/providers/openai/apiKey" }
|
||||
```
|
||||
|
||||
Validation:
|
||||
|
||||
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
|
||||
- `id` must be an absolute JSON pointer (`/...`)
|
||||
- RFC6901 escaping in segments: `~` => `~0`, `/` => `~1`
|
||||
|
||||
### `source: "exec"`
|
||||
|
||||
```json5
|
||||
{ source: "exec", provider: "vault", id: "providers/openai/apiKey" }
|
||||
```
|
||||
|
||||
Validation:
|
||||
|
||||
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
|
||||
- `id` must match `^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$`
|
||||
- `id` must not contain `.` or `..` as slash-delimited path segments (for example `a/../b` is rejected)
|
||||
|
||||
## Provider config
|
||||
|
||||
Define providers under `secrets.providers`:
|
||||
|
||||
```json5
|
||||
{
|
||||
secrets: {
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
filemain: {
|
||||
source: "file",
|
||||
path: "~/.openclaw/secrets.json",
|
||||
mode: "json", // or "singleValue"
|
||||
},
|
||||
vault: {
|
||||
source: "exec",
|
||||
command: "/usr/local/bin/openclaw-vault-resolver",
|
||||
args: ["--profile", "prod"],
|
||||
passEnv: ["PATH", "VAULT_ADDR"],
|
||||
jsonOnly: true,
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
env: "default",
|
||||
file: "filemain",
|
||||
exec: "vault",
|
||||
},
|
||||
resolution: {
|
||||
maxProviderConcurrency: 4,
|
||||
maxRefsPerProvider: 512,
|
||||
maxBatchBytes: 262144,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Env provider
|
||||
|
||||
- Optional allowlist via `allowlist`.
|
||||
- Missing/empty env values fail resolution.
|
||||
|
||||
### File provider
|
||||
|
||||
- Reads local file from `path`.
|
||||
- `mode: "json"` expects JSON object payload and resolves `id` as pointer.
|
||||
- `mode: "singleValue"` expects ref id `"value"` and returns file contents.
|
||||
- Path must pass ownership/permission checks.
|
||||
- Windows fail-closed note: if ACL verification is unavailable for a path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
|
||||
|
||||
### Exec provider
|
||||
|
||||
- Runs configured absolute binary path, no shell.
|
||||
- By default, `command` must point to a regular file (not a symlink).
|
||||
- Set `allowSymlinkCommand: true` to allow symlink command paths (for example Homebrew shims). OpenClaw validates the resolved target path.
|
||||
- Pair `allowSymlinkCommand` with `trustedDirs` for package-manager paths (for example `["/opt/homebrew"]`).
|
||||
- Supports timeout, no-output timeout, output byte limits, env allowlist, and trusted dirs.
|
||||
- Windows fail-closed note: if ACL verification is unavailable for the command path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
|
||||
|
||||
Request payload (stdin):
|
||||
|
||||
```json
|
||||
{ "protocolVersion": 1, "provider": "vault", "ids": ["providers/openai/apiKey"] }
|
||||
```
|
||||
|
||||
Response payload (stdout):
|
||||
|
||||
```jsonc
|
||||
{ "protocolVersion": 1, "values": { "providers/openai/apiKey": "<openai-api-key>" } } // pragma: allowlist secret
|
||||
```
|
||||
|
||||
Optional per-id errors:
|
||||
|
||||
```json
|
||||
{
|
||||
"protocolVersion": 1,
|
||||
"values": {},
|
||||
"errors": { "providers/openai/apiKey": { "message": "not found" } }
|
||||
}
|
||||
```
|
||||
|
||||
## Exec integration examples
|
||||
|
||||
### 1Password CLI
|
||||
|
||||
```json5
|
||||
{
|
||||
secrets: {
|
||||
providers: {
|
||||
onepassword_openai: {
|
||||
source: "exec",
|
||||
command: "/opt/homebrew/bin/op",
|
||||
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
|
||||
trustedDirs: ["/opt/homebrew"],
|
||||
args: ["read", "op://Personal/OpenClaw QA API Key/password"],
|
||||
passEnv: ["HOME"],
|
||||
jsonOnly: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
models: [{ id: "gpt-5", name: "gpt-5" }],
|
||||
apiKey: { source: "exec", provider: "onepassword_openai", id: "value" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### HashiCorp Vault CLI
|
||||
|
||||
```json5
|
||||
{
|
||||
secrets: {
|
||||
providers: {
|
||||
vault_openai: {
|
||||
source: "exec",
|
||||
command: "/opt/homebrew/bin/vault",
|
||||
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
|
||||
trustedDirs: ["/opt/homebrew"],
|
||||
args: ["kv", "get", "-field=OPENAI_API_KEY", "secret/openclaw"],
|
||||
passEnv: ["VAULT_ADDR", "VAULT_TOKEN"],
|
||||
jsonOnly: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
models: [{ id: "gpt-5", name: "gpt-5" }],
|
||||
apiKey: { source: "exec", provider: "vault_openai", id: "value" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### `sops`
|
||||
|
||||
```json5
|
||||
{
|
||||
secrets: {
|
||||
providers: {
|
||||
sops_openai: {
|
||||
source: "exec",
|
||||
command: "/opt/homebrew/bin/sops",
|
||||
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
|
||||
trustedDirs: ["/opt/homebrew"],
|
||||
args: ["-d", "--extract", '["providers"]["openai"]["apiKey"]', "/path/to/secrets.enc.json"],
|
||||
passEnv: ["SOPS_AGE_KEY_FILE"],
|
||||
jsonOnly: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
models: [{ id: "gpt-5", name: "gpt-5" }],
|
||||
apiKey: { source: "exec", provider: "sops_openai", id: "value" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## MCP server environment variables
|
||||
|
||||
MCP server env vars configured via `plugins.entries.acpx.config.mcpServers` support SecretInput. This keeps API keys and tokens out of plaintext config:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
acpx: {
|
||||
enabled: true,
|
||||
config: {
|
||||
mcpServers: {
|
||||
github: {
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-github"],
|
||||
env: {
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MCP_GITHUB_PAT",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Plaintext string values still work. Env-template refs like `${MCP_SERVER_API_KEY}` and SecretRef objects are resolved during gateway activation before the MCP server process is spawned. As with other SecretRef surfaces, unresolved refs only block activation when the `acpx` plugin is effectively active.
|
||||
|
||||
## Sandbox SSH auth material
|
||||
|
||||
The core `ssh` sandbox backend also supports SecretRefs for SSH auth material:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "ssh",
|
||||
ssh: {
|
||||
target: "user@gateway-host:22",
|
||||
identityData: { source: "env", provider: "default", id: "SSH_IDENTITY" },
|
||||
certificateData: { source: "env", provider: "default", id: "SSH_CERTIFICATE" },
|
||||
knownHostsData: { source: "env", provider: "default", id: "SSH_KNOWN_HOSTS" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Runtime behavior:
|
||||
|
||||
- OpenClaw resolves these refs during sandbox activation, not lazily during each SSH call.
|
||||
- Resolved values are written to temp files with restrictive permissions and used in generated SSH config.
|
||||
- If the effective sandbox backend is not `ssh`, these refs stay inactive and do not block startup.
|
||||
|
||||
## Supported credential surface
|
||||
|
||||
Canonical supported and unsupported credentials are listed in:
|
||||
|
||||
- [SecretRef Credential Surface](/reference/secretref-credential-surface)
|
||||
|
||||
Runtime-minted or rotating credentials and OAuth refresh material are intentionally excluded from read-only SecretRef resolution.
|
||||
|
||||
## Required behavior and precedence
|
||||
|
||||
- Field without a ref: unchanged.
|
||||
- Field with a ref: required on active surfaces during activation.
|
||||
- If both plaintext and ref are present, ref takes precedence on supported precedence paths.
|
||||
- The redaction sentinel `__OPENCLAW_REDACTED__` is reserved for internal config redaction/restore and is rejected as literal submitted config data.
|
||||
|
||||
Warning and audit signals:
|
||||
|
||||
- `SECRETS_REF_OVERRIDES_PLAINTEXT` (runtime warning)
|
||||
- `REF_SHADOWED` (audit finding when `auth-profiles.json` credentials take precedence over `openclaw.json` refs)
|
||||
|
||||
Google Chat compatibility behavior:
|
||||
|
||||
- `serviceAccountRef` takes precedence over plaintext `serviceAccount`.
|
||||
- Plaintext value is ignored when sibling ref is set.
|
||||
|
||||
## Activation triggers
|
||||
|
||||
Secret activation runs on:
|
||||
|
||||
- Startup (preflight plus final activation)
|
||||
- Config reload hot-apply path
|
||||
- Config reload restart-check path
|
||||
- Manual reload via `secrets.reload`
|
||||
- Gateway config write RPC preflight (`config.set` / `config.apply` / `config.patch`) for active-surface SecretRef resolvability within the submitted config payload before persisting edits
|
||||
|
||||
Activation contract:
|
||||
|
||||
- Success swaps the snapshot atomically.
|
||||
- Startup failure aborts gateway startup.
|
||||
- Runtime reload failure keeps the last-known-good snapshot.
|
||||
- Write-RPC preflight failure rejects the submitted config and keeps both disk config and active runtime snapshot unchanged.
|
||||
- Providing an explicit per-call channel token to an outbound helper/tool call does not trigger SecretRef activation; activation points remain startup, reload, and explicit `secrets.reload`.
|
||||
|
||||
## Degraded and recovered signals
|
||||
|
||||
When reload-time activation fails after a healthy state, OpenClaw enters degraded secrets state.
|
||||
|
||||
One-shot system event and log codes:
|
||||
|
||||
- `SECRETS_RELOADER_DEGRADED`
|
||||
- `SECRETS_RELOADER_RECOVERED`
|
||||
|
||||
Behavior:
|
||||
|
||||
- Degraded: runtime keeps last-known-good snapshot.
|
||||
- Recovered: emitted once after the next successful activation.
|
||||
- Repeated failures while already degraded log warnings but do not spam events.
|
||||
- Startup fail-fast does not emit degraded events because runtime never became active.
|
||||
|
||||
## Command-path resolution
|
||||
|
||||
Command paths can opt into supported SecretRef resolution via gateway snapshot RPC.
|
||||
|
||||
There are two broad behaviors:
|
||||
|
||||
- Strict command paths (for example `openclaw memory` remote-memory paths and `openclaw qr --remote` when it needs remote shared-secret refs) read from the active snapshot and fail fast when a required SecretRef is unavailable.
|
||||
- Read-only command paths (for example `openclaw status`, `openclaw status --all`, `openclaw channels status`, `openclaw channels resolve`, `openclaw security audit`, and read-only doctor/config repair flows) also prefer the active snapshot, but degrade instead of aborting when a targeted SecretRef is unavailable in that command path.
|
||||
|
||||
Read-only behavior:
|
||||
|
||||
- When the gateway is running, these commands read from the active snapshot first.
|
||||
- If gateway resolution is incomplete or the gateway is unavailable, they attempt targeted local fallback for the specific command surface.
|
||||
- If a targeted SecretRef is still unavailable, the command continues with degraded read-only output and explicit diagnostics such as “configured but unavailable in this command path”.
|
||||
- This degraded behavior is command-local only. It does not weaken runtime startup, reload, or send/auth paths.
|
||||
|
||||
Other notes:
|
||||
|
||||
- Snapshot refresh after backend secret rotation is handled by `openclaw secrets reload`.
|
||||
- Gateway RPC method used by these command paths: `secrets.resolve`.
|
||||
|
||||
## Audit and configure workflow
|
||||
|
||||
Default operator flow:
|
||||
|
||||
```bash
|
||||
openclaw secrets audit --check
|
||||
openclaw secrets configure
|
||||
openclaw secrets audit --check
|
||||
```
|
||||
|
||||
### `secrets audit`
|
||||
|
||||
Findings include:
|
||||
|
||||
- plaintext values at rest (`openclaw.json`, `auth-profiles.json`, `.env`, and generated `agents/*/agent/models.json`)
|
||||
- plaintext sensitive provider header residues in generated `models.json` entries
|
||||
- unresolved refs
|
||||
- precedence shadowing (`auth-profiles.json` taking priority over `openclaw.json` refs)
|
||||
- legacy residues (`auth.json`, OAuth reminders)
|
||||
|
||||
Exec note:
|
||||
|
||||
- By default, audit skips exec SecretRef resolvability checks to avoid command side effects.
|
||||
- Use `openclaw secrets audit --allow-exec` to execute exec providers during audit.
|
||||
|
||||
Header residue note:
|
||||
|
||||
- Sensitive provider header detection is name-heuristic based (common auth/credential header names and fragments such as `authorization`, `x-api-key`, `token`, `secret`, `password`, and `credential`).
|
||||
|
||||
### `secrets configure`
|
||||
|
||||
Interactive helper that:
|
||||
|
||||
- configures `secrets.providers` first (`env`/`file`/`exec`, add/edit/remove)
|
||||
- lets you select supported secret-bearing fields in `openclaw.json` plus `auth-profiles.json` for one agent scope
|
||||
- can create a new `auth-profiles.json` mapping directly in the target picker
|
||||
- captures SecretRef details (`source`, `provider`, `id`)
|
||||
- runs preflight resolution
|
||||
- can apply immediately
|
||||
|
||||
Exec note:
|
||||
|
||||
- Preflight skips exec SecretRef checks unless `--allow-exec` is set.
|
||||
- If you apply directly from `configure --apply` and the plan includes exec refs/providers, keep `--allow-exec` set for the apply step too.
|
||||
|
||||
Helpful modes:
|
||||
|
||||
- `openclaw secrets configure --providers-only`
|
||||
- `openclaw secrets configure --skip-provider-setup`
|
||||
- `openclaw secrets configure --agent <id>`
|
||||
|
||||
`configure` apply defaults:
|
||||
|
||||
- scrub matching static credentials from `auth-profiles.json` for targeted providers
|
||||
- scrub legacy static `api_key` entries from `auth.json`
|
||||
- scrub matching known secret lines from `<config-dir>/.env`
|
||||
|
||||
### `secrets apply`
|
||||
|
||||
Apply a saved plan:
|
||||
|
||||
```bash
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --allow-exec
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run
|
||||
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run --allow-exec
|
||||
```
|
||||
|
||||
Exec note:
|
||||
|
||||
- dry-run skips exec checks unless `--allow-exec` is set.
|
||||
- write mode rejects plans containing exec SecretRefs/providers unless `--allow-exec` is set.
|
||||
|
||||
For strict target/path contract details and exact rejection rules, see:
|
||||
|
||||
- [Secrets Apply Plan Contract](/gateway/secrets-plan-contract)
|
||||
|
||||
## One-way safety policy
|
||||
|
||||
OpenClaw intentionally does not write rollback backups containing historical plaintext secret values.
|
||||
|
||||
Safety model:
|
||||
|
||||
- preflight must succeed before write mode
|
||||
- runtime activation is validated before commit
|
||||
- apply updates files using atomic file replacement and best-effort restore on failure
|
||||
|
||||
## Legacy auth compatibility notes
|
||||
|
||||
For static credentials, runtime no longer depends on plaintext legacy auth storage.
|
||||
|
||||
- Runtime credential source is the resolved in-memory snapshot.
|
||||
- Legacy static `api_key` entries are scrubbed when discovered.
|
||||
- OAuth-related compatibility behavior remains separate.
|
||||
|
||||
## Web UI note
|
||||
|
||||
Some SecretInput unions are easier to configure in raw editor mode than in form mode.
|
||||
|
||||
## Related docs
|
||||
|
||||
- CLI commands: [secrets](/cli/secrets)
|
||||
- Plan contract details: [Secrets Apply Plan Contract](/gateway/secrets-plan-contract)
|
||||
- Credential surface: [SecretRef Credential Surface](/reference/secretref-credential-surface)
|
||||
- Auth setup: [Authentication](/gateway/authentication)
|
||||
- Security posture: [Security](/gateway/security)
|
||||
- Environment precedence: [Environment Variables](/help/environment)
|
||||
1365
openclaw/docs/gateway/security/index.md
Normal file
1365
openclaw/docs/gateway/security/index.md
Normal file
File diff suppressed because it is too large
Load diff
136
openclaw/docs/gateway/tailscale.md
Normal file
136
openclaw/docs/gateway/tailscale.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
summary: "Integrated Tailscale Serve/Funnel for the Gateway dashboard"
|
||||
read_when:
|
||||
- Exposing the Gateway Control UI outside localhost
|
||||
- Automating tailnet or public dashboard access
|
||||
title: "Tailscale"
|
||||
---
|
||||
|
||||
# Tailscale (Gateway dashboard)
|
||||
|
||||
OpenClaw can auto-configure Tailscale **Serve** (tailnet) or **Funnel** (public) for the
|
||||
Gateway dashboard and WebSocket port. This keeps the Gateway bound to loopback while
|
||||
Tailscale provides HTTPS, routing, and (for Serve) identity headers.
|
||||
|
||||
## Modes
|
||||
|
||||
- `serve`: Tailnet-only Serve via `tailscale serve`. The gateway stays on `127.0.0.1`.
|
||||
- `funnel`: Public HTTPS via `tailscale funnel`. OpenClaw requires a shared password.
|
||||
- `off`: Default (no Tailscale automation).
|
||||
|
||||
## Auth
|
||||
|
||||
Set `gateway.auth.mode` to control the handshake:
|
||||
|
||||
- `none` (private ingress only)
|
||||
- `token` (default when `OPENCLAW_GATEWAY_TOKEN` is set)
|
||||
- `password` (shared secret via `OPENCLAW_GATEWAY_PASSWORD` or config)
|
||||
- `trusted-proxy` (identity-aware reverse proxy; see [Trusted Proxy Auth](/gateway/trusted-proxy-auth))
|
||||
|
||||
When `tailscale.mode = "serve"` and `gateway.auth.allowTailscale` is `true`,
|
||||
Control UI/WebSocket auth can use Tailscale identity headers
|
||||
(`tailscale-user-login`) without supplying a token/password. OpenClaw verifies
|
||||
the identity by resolving the `x-forwarded-for` address via the local Tailscale
|
||||
daemon (`tailscale whois`) and matching it to the header before accepting it.
|
||||
OpenClaw only treats a request as Serve when it arrives from loopback with
|
||||
Tailscale’s `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host`
|
||||
headers.
|
||||
HTTP API endpoints (for example `/v1/*`, `/tools/invoke`, and `/api/channels/*`)
|
||||
do **not** use Tailscale identity-header auth. They still follow the gateway's
|
||||
normal HTTP auth mode: shared-secret auth by default, or an intentionally
|
||||
configured trusted-proxy / private-ingress `none` setup.
|
||||
This tokenless flow assumes the gateway host is trusted. If untrusted local code
|
||||
may run on the same host, disable `gateway.auth.allowTailscale` and require
|
||||
token/password auth instead.
|
||||
To require explicit shared-secret credentials, set `gateway.auth.allowTailscale: false`
|
||||
and use `gateway.auth.mode: "token"` or `"password"`.
|
||||
|
||||
## Config examples
|
||||
|
||||
### Tailnet-only (Serve)
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
bind: "loopback",
|
||||
tailscale: { mode: "serve" },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Open: `https://<magicdns>/` (or your configured `gateway.controlUi.basePath`)
|
||||
|
||||
### Tailnet-only (bind to Tailnet IP)
|
||||
|
||||
Use this when you want the Gateway to listen directly on the Tailnet IP (no Serve/Funnel).
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
bind: "tailnet",
|
||||
auth: { mode: "token", token: "your-token" },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Connect from another Tailnet device:
|
||||
|
||||
- Control UI: `http://<tailscale-ip>:18789/`
|
||||
- WebSocket: `ws://<tailscale-ip>:18789`
|
||||
|
||||
Note: loopback (`http://127.0.0.1:18789`) will **not** work in this mode.
|
||||
|
||||
### Public internet (Funnel + shared password)
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
bind: "loopback",
|
||||
tailscale: { mode: "funnel" },
|
||||
auth: { mode: "password", password: "replace-me" },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Prefer `OPENCLAW_GATEWAY_PASSWORD` over committing a password to disk.
|
||||
|
||||
## CLI examples
|
||||
|
||||
```bash
|
||||
openclaw gateway --tailscale serve
|
||||
openclaw gateway --tailscale funnel --auth password
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Tailscale Serve/Funnel requires the `tailscale` CLI to be installed and logged in.
|
||||
- `tailscale.mode: "funnel"` refuses to start unless auth mode is `password` to avoid public exposure.
|
||||
- Set `gateway.tailscale.resetOnExit` if you want OpenClaw to undo `tailscale serve`
|
||||
or `tailscale funnel` configuration on shutdown.
|
||||
- `gateway.bind: "tailnet"` is a direct Tailnet bind (no HTTPS, no Serve/Funnel).
|
||||
- `gateway.bind: "auto"` prefers loopback; use `tailnet` if you want Tailnet-only.
|
||||
- Serve/Funnel only expose the **Gateway control UI + WS**. Nodes connect over
|
||||
the same Gateway WS endpoint, so Serve can work for node access.
|
||||
|
||||
## Browser control (remote Gateway + local browser)
|
||||
|
||||
If you run the Gateway on one machine but want to drive a browser on another machine,
|
||||
run a **node host** on the browser machine and keep both on the same tailnet.
|
||||
The Gateway will proxy browser actions to the node; no separate control server or Serve URL needed.
|
||||
|
||||
Avoid Funnel for browser control; treat node pairing like operator access.
|
||||
|
||||
## Tailscale prerequisites + limits
|
||||
|
||||
- Serve requires HTTPS enabled for your tailnet; the CLI prompts if it is missing.
|
||||
- Serve injects Tailscale identity headers; Funnel does not.
|
||||
- Funnel requires Tailscale v1.38.3+, MagicDNS, HTTPS enabled, and a funnel node attribute.
|
||||
- Funnel only supports ports `443`, `8443`, and `10000` over TLS.
|
||||
- Funnel on macOS requires the open-source Tailscale app variant.
|
||||
|
||||
## Learn more
|
||||
|
||||
- Tailscale Serve overview: [https://tailscale.com/kb/1312/serve](https://tailscale.com/kb/1312/serve)
|
||||
- `tailscale serve` command: [https://tailscale.com/kb/1242/tailscale-serve](https://tailscale.com/kb/1242/tailscale-serve)
|
||||
- Tailscale Funnel overview: [https://tailscale.com/kb/1223/tailscale-funnel](https://tailscale.com/kb/1223/tailscale-funnel)
|
||||
- `tailscale funnel` command: [https://tailscale.com/kb/1311/tailscale-funnel](https://tailscale.com/kb/1311/tailscale-funnel)
|
||||
161
openclaw/docs/gateway/tools-invoke-http-api.md
Normal file
161
openclaw/docs/gateway/tools-invoke-http-api.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
---
|
||||
summary: "Invoke a single tool directly via the Gateway HTTP endpoint"
|
||||
read_when:
|
||||
- Calling tools without running a full agent turn
|
||||
- Building automations that need tool policy enforcement
|
||||
title: "Tools Invoke API"
|
||||
---
|
||||
|
||||
# Tools Invoke (HTTP)
|
||||
|
||||
OpenClaw’s Gateway exposes a simple HTTP endpoint for invoking a single tool directly. It is always enabled and uses Gateway auth plus tool policy. Like the OpenAI-compatible `/v1/*` surface, shared-secret bearer auth is treated as trusted operator access for the whole gateway.
|
||||
|
||||
- `POST /tools/invoke`
|
||||
- Same port as the Gateway (WS + HTTP multiplex): `http://<gateway-host>:<port>/tools/invoke`
|
||||
|
||||
Default max payload size is 2 MB.
|
||||
|
||||
## Authentication
|
||||
|
||||
Uses the Gateway auth configuration.
|
||||
|
||||
Common HTTP auth paths:
|
||||
|
||||
- shared-secret auth (`gateway.auth.mode="token"` or `"password"`):
|
||||
`Authorization: Bearer <token-or-password>`
|
||||
- trusted identity-bearing HTTP auth (`gateway.auth.mode="trusted-proxy"`):
|
||||
route through the configured identity-aware proxy and let it inject the
|
||||
required identity headers
|
||||
- private-ingress open auth (`gateway.auth.mode="none"`):
|
||||
no auth header required
|
||||
|
||||
Notes:
|
||||
|
||||
- When `gateway.auth.mode="token"`, use `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`).
|
||||
- When `gateway.auth.mode="password"`, use `gateway.auth.password` (or `OPENCLAW_GATEWAY_PASSWORD`).
|
||||
- When `gateway.auth.mode="trusted-proxy"`, the HTTP request must come from a
|
||||
configured non-loopback trusted proxy source; same-host loopback proxies do
|
||||
not satisfy this mode.
|
||||
- If `gateway.auth.rateLimit` is configured and too many auth failures occur, the endpoint returns `429` with `Retry-After`.
|
||||
|
||||
## Security boundary (important)
|
||||
|
||||
Treat this endpoint as a **full operator-access** surface for the gateway instance.
|
||||
|
||||
- HTTP bearer auth here is not a narrow per-user scope model.
|
||||
- A valid Gateway token/password for this endpoint should be treated like an owner/operator credential.
|
||||
- For shared-secret auth modes (`token` and `password`), the endpoint restores the normal full operator defaults even if the caller sends a narrower `x-openclaw-scopes` header.
|
||||
- Shared-secret auth also treats direct tool invokes on this endpoint as owner-sender turns.
|
||||
- Trusted identity-bearing HTTP modes (for example trusted proxy auth or `gateway.auth.mode="none"` on a private ingress) honor `x-openclaw-scopes` when present and otherwise fall back to the normal operator default scope set.
|
||||
- Keep this endpoint on loopback/tailnet/private ingress only; do not expose it directly to the public internet.
|
||||
|
||||
Auth matrix:
|
||||
|
||||
- `gateway.auth.mode="token"` or `"password"` + `Authorization: Bearer ...`
|
||||
- proves possession of the shared gateway operator secret
|
||||
- ignores narrower `x-openclaw-scopes`
|
||||
- restores the full default operator scope set:
|
||||
`operator.admin`, `operator.approvals`, `operator.pairing`,
|
||||
`operator.read`, `operator.talk.secrets`, `operator.write`
|
||||
- treats direct tool invokes on this endpoint as owner-sender turns
|
||||
- trusted identity-bearing HTTP modes (for example trusted proxy auth, or `gateway.auth.mode="none"` on private ingress)
|
||||
- authenticate some outer trusted identity or deployment boundary
|
||||
- honor `x-openclaw-scopes` when the header is present
|
||||
- fall back to the normal operator default scope set when the header is absent
|
||||
- only lose owner semantics when the caller explicitly narrows scopes and omits `operator.admin`
|
||||
|
||||
## Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"tool": "sessions_list",
|
||||
"action": "json",
|
||||
"args": {},
|
||||
"sessionKey": "main",
|
||||
"dryRun": false
|
||||
}
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `tool` (string, required): tool name to invoke.
|
||||
- `action` (string, optional): mapped into args if the tool schema supports `action` and the args payload omitted it.
|
||||
- `args` (object, optional): tool-specific arguments.
|
||||
- `sessionKey` (string, optional): target session key. If omitted or `"main"`, the Gateway uses the configured main session key (honors `session.mainKey` and default agent, or `global` in global scope).
|
||||
- `dryRun` (boolean, optional): reserved for future use; currently ignored.
|
||||
|
||||
## Policy + routing behavior
|
||||
|
||||
Tool availability is filtered through the same policy chain used by Gateway agents:
|
||||
|
||||
- `tools.profile` / `tools.byProvider.profile`
|
||||
- `tools.allow` / `tools.byProvider.allow`
|
||||
- `agents.<id>.tools.allow` / `agents.<id>.tools.byProvider.allow`
|
||||
- group policies (if the session key maps to a group or channel)
|
||||
- subagent policy (when invoking with a subagent session key)
|
||||
|
||||
If a tool is not allowed by policy, the endpoint returns **404**.
|
||||
|
||||
Important boundary notes:
|
||||
|
||||
- Exec approvals are operator guardrails, not a separate authorization boundary for this HTTP endpoint. If a tool is reachable here via Gateway auth + tool policy, `/tools/invoke` does not add an extra per-call approval prompt.
|
||||
- Do not share Gateway bearer credentials with untrusted callers. If you need separation across trust boundaries, run separate gateways (and ideally separate OS users/hosts).
|
||||
|
||||
Gateway HTTP also applies a hard deny list by default (even if session policy allows the tool):
|
||||
|
||||
- `exec` — direct command execution (RCE surface)
|
||||
- `spawn` — arbitrary child process creation (RCE surface)
|
||||
- `shell` — shell command execution (RCE surface)
|
||||
- `fs_write` — arbitrary file mutation on the host
|
||||
- `fs_delete` — arbitrary file deletion on the host
|
||||
- `fs_move` — arbitrary file move/rename on the host
|
||||
- `apply_patch` — patch application can rewrite arbitrary files
|
||||
- `sessions_spawn` — session orchestration; spawning agents remotely is RCE
|
||||
- `sessions_send` — cross-session message injection
|
||||
- `cron` — persistent automation control plane
|
||||
- `gateway` — gateway control plane; prevents reconfiguration via HTTP
|
||||
- `nodes` — node command relay can reach system.run on paired hosts
|
||||
- `whatsapp_login` — interactive setup requiring terminal QR scan; hangs on HTTP
|
||||
|
||||
You can customize this deny list via `gateway.tools`:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
tools: {
|
||||
// Additional tools to block over HTTP /tools/invoke
|
||||
deny: ["browser"],
|
||||
// Remove tools from the default deny list
|
||||
allow: ["gateway"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
To help group policies resolve context, you can optionally set:
|
||||
|
||||
- `x-openclaw-message-channel: <channel>` (example: `slack`, `telegram`)
|
||||
- `x-openclaw-account-id: <accountId>` (when multiple accounts exist)
|
||||
|
||||
## Responses
|
||||
|
||||
- `200` → `{ ok: true, result }`
|
||||
- `400` → `{ ok: false, error: { type, message } }` (invalid request or tool input error)
|
||||
- `401` → unauthorized
|
||||
- `429` → auth rate-limited (`Retry-After` set)
|
||||
- `404` → tool not available (not found or not allowlisted)
|
||||
- `405` → method not allowed
|
||||
- `500` → `{ ok: false, error: { type, message } }` (unexpected tool execution error; sanitized message)
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
curl -sS http://127.0.0.1:18789/tools/invoke \
|
||||
-H 'Authorization: Bearer secret' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"tool": "sessions_list",
|
||||
"action": "json",
|
||||
"args": {}
|
||||
}'
|
||||
```
|
||||
564
openclaw/docs/gateway/troubleshooting.md
Normal file
564
openclaw/docs/gateway/troubleshooting.md
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
---
|
||||
summary: "Deep troubleshooting runbook for gateway, channels, automation, nodes, and browser"
|
||||
read_when:
|
||||
- The troubleshooting hub pointed you here for deeper diagnosis
|
||||
- You need stable symptom based runbook sections with exact commands
|
||||
title: "Troubleshooting"
|
||||
---
|
||||
|
||||
# Gateway troubleshooting
|
||||
|
||||
This page is the deep runbook.
|
||||
Start at [/help/troubleshooting](/help/troubleshooting) if you want the fast triage flow first.
|
||||
|
||||
## Command ladder
|
||||
|
||||
Run these first, in this order:
|
||||
|
||||
```bash
|
||||
openclaw status
|
||||
openclaw gateway status
|
||||
openclaw logs --follow
|
||||
openclaw doctor
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
Expected healthy signals:
|
||||
|
||||
- `openclaw gateway status` shows `Runtime: running`, `Connectivity probe: ok`, and a `Capability: ...` line.
|
||||
- `openclaw doctor` reports no blocking config/service issues.
|
||||
- `openclaw channels status --probe` shows live per-account transport status and,
|
||||
where supported, probe/audit results such as `works` or `audit ok`.
|
||||
|
||||
## Anthropic 429 extra usage required for long context
|
||||
|
||||
Use this when logs/errors include:
|
||||
`HTTP 429: rate_limit_error: Extra usage is required for long context requests`.
|
||||
|
||||
```bash
|
||||
openclaw logs --follow
|
||||
openclaw models status
|
||||
openclaw config get agents.defaults.models
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- Selected Anthropic Opus/Sonnet model has `params.context1m: true`.
|
||||
- Current Anthropic credential is not eligible for long-context usage.
|
||||
- Requests fail only on long sessions/model runs that need the 1M beta path.
|
||||
|
||||
Fix options:
|
||||
|
||||
1. Disable `context1m` for that model to fall back to the normal context window.
|
||||
2. Use an Anthropic credential that is eligible for long-context requests, or switch to an Anthropic API key.
|
||||
3. Configure fallback models so runs continue when Anthropic long-context requests are rejected.
|
||||
|
||||
Related:
|
||||
|
||||
- [/providers/anthropic](/providers/anthropic)
|
||||
- [/reference/token-use](/reference/token-use)
|
||||
- [/help/faq#why-am-i-seeing-http-429-ratelimiterror-from-anthropic](/help/faq#why-am-i-seeing-http-429-ratelimiterror-from-anthropic)
|
||||
|
||||
## Local OpenAI-compatible backend passes direct probes but agent runs fail
|
||||
|
||||
Use this when:
|
||||
|
||||
- `curl ... /v1/models` works
|
||||
- tiny direct `/v1/chat/completions` calls work
|
||||
- OpenClaw model runs fail only on normal agent turns
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:1234/v1/models
|
||||
curl http://127.0.0.1:1234/v1/chat/completions \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"model":"<id>","messages":[{"role":"user","content":"hi"}],"stream":false}'
|
||||
openclaw infer model run --model <provider/model> --prompt "hi" --json
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- direct tiny calls succeed, but OpenClaw runs fail only on larger prompts
|
||||
- backend errors about `messages[].content` expecting a string
|
||||
- backend crashes that appear only with larger prompt-token counts or full agent
|
||||
runtime prompts
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `messages[...].content: invalid type: sequence, expected a string` → backend
|
||||
rejects structured Chat Completions content parts. Fix: set
|
||||
`models.providers.<provider>.models[].compat.requiresStringContent: true`.
|
||||
- direct tiny requests succeed, but OpenClaw agent runs fail with backend/model
|
||||
crashes (for example Gemma on some `inferrs` builds) → OpenClaw transport is
|
||||
likely already correct; the backend is failing on the larger agent-runtime
|
||||
prompt shape.
|
||||
- failures shrink after disabling tools but do not disappear → tool schemas were
|
||||
part of the pressure, but the remaining issue is still upstream model/server
|
||||
capacity or a backend bug.
|
||||
|
||||
Fix options:
|
||||
|
||||
1. Set `compat.requiresStringContent: true` for string-only Chat Completions backends.
|
||||
2. Set `compat.supportsTools: false` for models/backends that cannot handle
|
||||
OpenClaw's tool schema surface reliably.
|
||||
3. Lower prompt pressure where possible: smaller workspace bootstrap, shorter
|
||||
session history, lighter local model, or a backend with stronger long-context
|
||||
support.
|
||||
4. If tiny direct requests keep passing while OpenClaw agent turns still crash
|
||||
inside the backend, treat it as an upstream server/model limitation and file
|
||||
a repro there with the accepted payload shape.
|
||||
|
||||
Related:
|
||||
|
||||
- [/gateway/local-models](/gateway/local-models)
|
||||
- [/gateway/configuration](/gateway/configuration)
|
||||
- [/gateway/configuration-reference#openai-compatible-endpoints](/gateway/configuration-reference#openai-compatible-endpoints)
|
||||
|
||||
## No replies
|
||||
|
||||
If channels are up but nothing answers, check routing and policy before reconnecting anything.
|
||||
|
||||
```bash
|
||||
openclaw status
|
||||
openclaw channels status --probe
|
||||
openclaw pairing list --channel <channel> [--account <id>]
|
||||
openclaw config get channels
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- Pairing pending for DM senders.
|
||||
- Group mention gating (`requireMention`, `mentionPatterns`).
|
||||
- Channel/group allowlist mismatches.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `drop guild message (mention required` → group message ignored until mention.
|
||||
- `pairing request` → sender needs approval.
|
||||
- `blocked` / `allowlist` → sender/channel was filtered by policy.
|
||||
|
||||
Related:
|
||||
|
||||
- [/channels/troubleshooting](/channels/troubleshooting)
|
||||
- [/channels/pairing](/channels/pairing)
|
||||
- [/channels/groups](/channels/groups)
|
||||
|
||||
## Dashboard control ui connectivity
|
||||
|
||||
When dashboard/control UI will not connect, validate URL, auth mode, and secure context assumptions.
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
openclaw status
|
||||
openclaw logs --follow
|
||||
openclaw doctor
|
||||
openclaw gateway status --json
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- Correct probe URL and dashboard URL.
|
||||
- Auth mode/token mismatch between client and gateway.
|
||||
- HTTP usage where device identity is required.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `device identity required` → non-secure context or missing device auth.
|
||||
- `origin not allowed` → browser `Origin` is not in `gateway.controlUi.allowedOrigins`
|
||||
(or you are connecting from a non-loopback browser origin without an explicit
|
||||
allowlist).
|
||||
- `device nonce required` / `device nonce mismatch` → client is not completing the
|
||||
challenge-based device auth flow (`connect.challenge` + `device.nonce`).
|
||||
- `device signature invalid` / `device signature expired` → client signed the wrong
|
||||
payload (or stale timestamp) for the current handshake.
|
||||
- `AUTH_TOKEN_MISMATCH` with `canRetryWithDeviceToken=true` → client can do one trusted retry with cached device token.
|
||||
- That cached-token retry reuses the cached scope set stored with the paired
|
||||
device token. Explicit `deviceToken` / explicit `scopes` callers keep their
|
||||
requested scope set instead.
|
||||
- Outside that retry path, connect auth precedence is explicit shared
|
||||
token/password first, then explicit `deviceToken`, then stored device token,
|
||||
then bootstrap token.
|
||||
- On the async Tailscale Serve Control UI path, failed attempts for the same
|
||||
`{scope, ip}` are serialized before the limiter records the failure. Two bad
|
||||
concurrent retries from the same client can therefore surface `retry later`
|
||||
on the second attempt instead of two plain mismatches.
|
||||
- `too many failed authentication attempts (retry later)` from a browser-origin
|
||||
loopback client → repeated failures from that same normalized `Origin` are
|
||||
locked out temporarily; another localhost origin uses a separate bucket.
|
||||
- repeated `unauthorized` after that retry → shared token/device token drift; refresh token config and re-approve/rotate device token if needed.
|
||||
- `gateway connect failed:` → wrong host/port/url target.
|
||||
|
||||
### Auth detail codes quick map
|
||||
|
||||
Use `error.details.code` from the failed `connect` response to pick the next action:
|
||||
|
||||
| Detail code | Meaning | Recommended action |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `AUTH_TOKEN_MISSING` | Client did not send a required shared token. | Paste/set token in the client and retry. For dashboard paths: `openclaw config get gateway.auth.token` then paste into Control UI settings. |
|
||||
| `AUTH_TOKEN_MISMATCH` | Shared token did not match gateway auth token. | If `canRetryWithDeviceToken=true`, allow one trusted retry. Cached-token retries reuse stored approved scopes; explicit `deviceToken` / `scopes` callers keep requested scopes. If still failing, run the [token drift recovery checklist](/cli/devices#token-drift-recovery-checklist). |
|
||||
| `AUTH_DEVICE_TOKEN_MISMATCH` | Cached per-device token is stale or revoked. | Rotate/re-approve device token using [devices CLI](/cli/devices), then reconnect. |
|
||||
| `PAIRING_REQUIRED` | Device identity needs approval. Check `error.details.reason` for `not-paired`, `scope-upgrade`, `role-upgrade`, or `metadata-upgrade`, and use `requestId` / `remediationHint` when present. | Approve pending request: `openclaw devices list` then `openclaw devices approve <requestId>`. Scope/role upgrades use the same flow after you review the requested access. |
|
||||
|
||||
Device auth v2 migration check:
|
||||
|
||||
```bash
|
||||
openclaw --version
|
||||
openclaw doctor
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
If logs show nonce/signature errors, update the connecting client and verify it:
|
||||
|
||||
1. waits for `connect.challenge`
|
||||
2. signs the challenge-bound payload
|
||||
3. sends `connect.params.device.nonce` with the same challenge nonce
|
||||
|
||||
If `openclaw devices rotate` / `revoke` / `remove` is denied unexpectedly:
|
||||
|
||||
- paired-device token sessions can manage only **their own** device unless the
|
||||
caller also has `operator.admin`
|
||||
- `openclaw devices rotate --scope ...` can only request operator scopes that
|
||||
the caller session already holds
|
||||
|
||||
Related:
|
||||
|
||||
- [/web/control-ui](/web/control-ui)
|
||||
- [/gateway/configuration](/gateway/configuration) (gateway auth modes)
|
||||
- [/gateway/trusted-proxy-auth](/gateway/trusted-proxy-auth)
|
||||
- [/gateway/remote](/gateway/remote)
|
||||
- [/cli/devices](/cli/devices)
|
||||
|
||||
## Gateway service not running
|
||||
|
||||
Use this when service is installed but process does not stay up.
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
openclaw status
|
||||
openclaw logs --follow
|
||||
openclaw doctor
|
||||
openclaw gateway status --deep # also scan system-level services
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- `Runtime: stopped` with exit hints.
|
||||
- Service config mismatch (`Config (cli)` vs `Config (service)`).
|
||||
- Port/listener conflicts.
|
||||
- Extra launchd/systemd/schtasks installs when `--deep` is used.
|
||||
- `Other gateway-like services detected (best effort)` cleanup hints.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `Gateway start blocked: set gateway.mode=local` or `existing config is missing gateway.mode` → local gateway mode is not enabled, or the config file was clobbered and lost `gateway.mode`. Fix: set `gateway.mode="local"` in your config, or re-run `openclaw onboard --mode local` / `openclaw setup` to restamp the expected local-mode config. If you are running OpenClaw via Podman, the default config path is `~/.openclaw/openclaw.json`.
|
||||
- `refusing to bind gateway ... without auth` → non-loopback bind without a valid gateway auth path (token/password, or trusted-proxy where configured).
|
||||
- `another gateway instance is already listening` / `EADDRINUSE` → port conflict.
|
||||
- `Other gateway-like services detected (best effort)` → stale or parallel launchd/systemd/schtasks units exist. Most setups should keep one gateway per machine; if you do need more than one, isolate ports + config/state/workspace. See [/gateway#multiple-gateways-same-host](/gateway#multiple-gateways-same-host).
|
||||
|
||||
Related:
|
||||
|
||||
- [/gateway/background-process](/gateway/background-process)
|
||||
- [/gateway/configuration](/gateway/configuration)
|
||||
- [/gateway/doctor](/gateway/doctor)
|
||||
|
||||
## Gateway restored last-known-good config
|
||||
|
||||
Use this when the Gateway starts, but logs say it restored `openclaw.json`.
|
||||
|
||||
```bash
|
||||
openclaw logs --follow
|
||||
openclaw config file
|
||||
openclaw config validate
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- `Config auto-restored from last-known-good`
|
||||
- `gateway: invalid config was restored from last-known-good backup`
|
||||
- `config reload restored last-known-good config after invalid-config`
|
||||
- A timestamped `openclaw.json.clobbered.*` file beside the active config
|
||||
- A main-agent system event that starts with `Config recovery warning`
|
||||
|
||||
What happened:
|
||||
|
||||
- The rejected config did not validate during startup or hot reload.
|
||||
- OpenClaw preserved the rejected payload as `.clobbered.*`.
|
||||
- The active config was restored from the last validated last-known-good copy.
|
||||
- The next main-agent turn is warned not to blindly rewrite the rejected config.
|
||||
|
||||
Inspect and repair:
|
||||
|
||||
```bash
|
||||
CONFIG="$(openclaw config file)"
|
||||
ls -lt "$CONFIG".clobbered.* "$CONFIG".rejected.* 2>/dev/null | head
|
||||
diff -u "$CONFIG" "$(ls -t "$CONFIG".clobbered.* 2>/dev/null | head -n 1)"
|
||||
openclaw config validate
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `.clobbered.*` exists → an external direct edit or startup read was restored.
|
||||
- `.rejected.*` exists → an OpenClaw-owned config write failed schema or clobber checks before commit.
|
||||
- `Config write rejected:` → the write tried to drop required shape, shrink the file sharply, or persist invalid config.
|
||||
- `Config last-known-good promotion skipped` → the candidate contained redacted secret placeholders such as `***`.
|
||||
|
||||
Fix options:
|
||||
|
||||
1. Keep the restored active config if it is correct.
|
||||
2. Copy only the intended keys from `.clobbered.*` or `.rejected.*`, then apply them with `openclaw config set` or `config.patch`.
|
||||
3. Run `openclaw config validate` before restarting.
|
||||
4. If you edit by hand, keep the full JSON5 config, not just the partial object you wanted to change.
|
||||
|
||||
Related:
|
||||
|
||||
- [/gateway/configuration#strict-validation](/gateway/configuration#strict-validation)
|
||||
- [/gateway/configuration#config-hot-reload](/gateway/configuration#config-hot-reload)
|
||||
- [/cli/config](/cli/config)
|
||||
- [/gateway/doctor](/gateway/doctor)
|
||||
|
||||
## Gateway probe warnings
|
||||
|
||||
Use this when `openclaw gateway probe` reaches something, but still prints a warning block.
|
||||
|
||||
```bash
|
||||
openclaw gateway probe
|
||||
openclaw gateway probe --json
|
||||
openclaw gateway probe --ssh user@gateway-host
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- `warnings[].code` and `primaryTargetId` in JSON output.
|
||||
- Whether the warning is about SSH fallback, multiple gateways, missing scopes, or unresolved auth refs.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `SSH tunnel failed to start; falling back to direct probes.` → SSH setup failed, but the command still tried direct configured/loopback targets.
|
||||
- `multiple reachable gateways detected` → more than one target answered. Usually this means an intentional multi-gateway setup or stale/duplicate listeners.
|
||||
- `Read-probe diagnostics are limited by gateway scopes (missing operator.read)` → connect worked, but detail RPC is scope-limited; pair device identity or use credentials with `operator.read`.
|
||||
- `Capability: pairing-pending` or `gateway closed (1008): pairing required` → the gateway answered, but this client still needs pairing/approval before normal operator access.
|
||||
- unresolved `gateway.auth.*` / `gateway.remote.*` SecretRef warning text → auth material was unavailable in this command path for the failed target.
|
||||
|
||||
Related:
|
||||
|
||||
- [/cli/gateway](/cli/gateway)
|
||||
- [/gateway#multiple-gateways-same-host](/gateway#multiple-gateways-same-host)
|
||||
- [/gateway/remote](/gateway/remote)
|
||||
|
||||
## Channel connected messages not flowing
|
||||
|
||||
If channel state is connected but message flow is dead, focus on policy, permissions, and channel specific delivery rules.
|
||||
|
||||
```bash
|
||||
openclaw channels status --probe
|
||||
openclaw pairing list --channel <channel> [--account <id>]
|
||||
openclaw status --deep
|
||||
openclaw logs --follow
|
||||
openclaw config get channels
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- DM policy (`pairing`, `allowlist`, `open`, `disabled`).
|
||||
- Group allowlist and mention requirements.
|
||||
- Missing channel API permissions/scopes.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `mention required` → message ignored by group mention policy.
|
||||
- `pairing` / pending approval traces → sender is not approved.
|
||||
- `missing_scope`, `not_in_channel`, `Forbidden`, `401/403` → channel auth/permissions issue.
|
||||
|
||||
Related:
|
||||
|
||||
- [/channels/troubleshooting](/channels/troubleshooting)
|
||||
- [/channels/whatsapp](/channels/whatsapp)
|
||||
- [/channels/telegram](/channels/telegram)
|
||||
- [/channels/discord](/channels/discord)
|
||||
|
||||
## Cron and heartbeat delivery
|
||||
|
||||
If cron or heartbeat did not run or did not deliver, verify scheduler state first, then delivery target.
|
||||
|
||||
```bash
|
||||
openclaw cron status
|
||||
openclaw cron list
|
||||
openclaw cron runs --id <jobId> --limit 20
|
||||
openclaw system heartbeat last
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- Cron enabled and next wake present.
|
||||
- Job run history status (`ok`, `skipped`, `error`).
|
||||
- Heartbeat skip reasons (`quiet-hours`, `requests-in-flight`, `alerts-disabled`, `empty-heartbeat-file`, `no-tasks-due`).
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `cron: scheduler disabled; jobs will not run automatically` → cron disabled.
|
||||
- `cron: timer tick failed` → scheduler tick failed; check file/log/runtime errors.
|
||||
- `heartbeat skipped` with `reason=quiet-hours` → outside active hours window.
|
||||
- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank lines / markdown headers, so OpenClaw skips the model call.
|
||||
- `heartbeat skipped` with `reason=no-tasks-due` → `HEARTBEAT.md` contains a `tasks:` block, but none of the tasks are due on this tick.
|
||||
- `heartbeat: unknown accountId` → invalid account id for heartbeat delivery target.
|
||||
- `heartbeat skipped` with `reason=dm-blocked` → heartbeat target resolved to a DM-style destination while `agents.defaults.heartbeat.directPolicy` (or per-agent override) is set to `block`.
|
||||
|
||||
Related:
|
||||
|
||||
- [/automation/cron-jobs#troubleshooting](/automation/cron-jobs#troubleshooting)
|
||||
- [/automation/cron-jobs](/automation/cron-jobs)
|
||||
- [/gateway/heartbeat](/gateway/heartbeat)
|
||||
|
||||
## Node paired tool fails
|
||||
|
||||
If a node is paired but tools fail, isolate foreground, permission, and approval state.
|
||||
|
||||
```bash
|
||||
openclaw nodes status
|
||||
openclaw nodes describe --node <idOrNameOrIp>
|
||||
openclaw approvals get --node <idOrNameOrIp>
|
||||
openclaw logs --follow
|
||||
openclaw status
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- Node online with expected capabilities.
|
||||
- OS permission grants for camera/mic/location/screen.
|
||||
- Exec approvals and allowlist state.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `NODE_BACKGROUND_UNAVAILABLE` → node app must be in foreground.
|
||||
- `*_PERMISSION_REQUIRED` / `LOCATION_PERMISSION_REQUIRED` → missing OS permission.
|
||||
- `SYSTEM_RUN_DENIED: approval required` → exec approval pending.
|
||||
- `SYSTEM_RUN_DENIED: allowlist miss` → command blocked by allowlist.
|
||||
|
||||
Related:
|
||||
|
||||
- [/nodes/troubleshooting](/nodes/troubleshooting)
|
||||
- [/nodes/index](/nodes/index)
|
||||
- [/tools/exec-approvals](/tools/exec-approvals)
|
||||
|
||||
## Browser tool fails
|
||||
|
||||
Use this when browser tool actions fail even though the gateway itself is healthy.
|
||||
|
||||
```bash
|
||||
openclaw browser status
|
||||
openclaw browser start --browser-profile openclaw
|
||||
openclaw browser profiles
|
||||
openclaw logs --follow
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- Whether `plugins.allow` is set and includes `browser`.
|
||||
- Valid browser executable path.
|
||||
- CDP profile reachability.
|
||||
- Local Chrome availability for `existing-session` / `user` profiles.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `unknown command "browser"` or `unknown command 'browser'` → the bundled browser plugin is excluded by `plugins.allow`.
|
||||
- browser tool missing / unavailable while `browser.enabled=true` → `plugins.allow` excludes `browser`, so the plugin never loaded.
|
||||
- `Failed to start Chrome CDP on port` → browser process failed to launch.
|
||||
- `browser.executablePath not found` → configured path is invalid.
|
||||
- `browser.cdpUrl must be http(s) or ws(s)` → the configured CDP URL uses an unsupported scheme such as `file:` or `ftp:`.
|
||||
- `browser.cdpUrl has invalid port` → the configured CDP URL has a bad or out-of-range port.
|
||||
- `No Chrome tabs found for profile="user"` → the Chrome MCP attach profile has no open local Chrome tabs.
|
||||
- `Remote CDP for profile "<name>" is not reachable` → the configured remote CDP endpoint is not reachable from the gateway host.
|
||||
- `Browser attachOnly is enabled ... not reachable` or `Browser attachOnly is enabled and CDP websocket ... is not reachable` → attach-only profile has no reachable target, or the HTTP endpoint answered but the CDP WebSocket still could not be opened.
|
||||
- `Playwright is not available in this gateway build; '<feature>' is unsupported.` → the current gateway install lacks the full Playwright package; ARIA snapshots and basic page screenshots can still work, but navigation, AI snapshots, CSS-selector element screenshots, and PDF export stay unavailable.
|
||||
- `fullPage is not supported for element screenshots` → screenshot request mixed `--full-page` with `--ref` or `--element`.
|
||||
- `element screenshots are not supported for existing-session profiles; use ref from snapshot.` → Chrome MCP / `existing-session` screenshot calls must use page capture or a snapshot `--ref`, not CSS `--element`.
|
||||
- `existing-session file uploads do not support element selectors; use ref/inputRef.` → Chrome MCP upload hooks need snapshot refs, not CSS selectors.
|
||||
- `existing-session file uploads currently support one file at a time.` → send one upload per call on Chrome MCP profiles.
|
||||
- `existing-session dialog handling does not support timeoutMs.` → dialog hooks on Chrome MCP profiles do not support timeout overrides.
|
||||
- `response body is not supported for existing-session profiles yet.` → `responsebody` still requires a managed browser or raw CDP profile.
|
||||
- stale viewport / dark-mode / locale / offline overrides on attach-only or remote CDP profiles → run `openclaw browser stop --browser-profile <name>` to close the active control session and release Playwright/CDP emulation state without restarting the whole gateway.
|
||||
|
||||
Related:
|
||||
|
||||
- [/tools/browser-linux-troubleshooting](/tools/browser-linux-troubleshooting)
|
||||
- [/tools/browser](/tools/browser)
|
||||
|
||||
## If you upgraded and something suddenly broke
|
||||
|
||||
Most post-upgrade breakage is config drift or stricter defaults now being enforced.
|
||||
|
||||
### 1) Auth and URL override behavior changed
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
openclaw config get gateway.mode
|
||||
openclaw config get gateway.remote.url
|
||||
openclaw config get gateway.auth.mode
|
||||
```
|
||||
|
||||
What to check:
|
||||
|
||||
- If `gateway.mode=remote`, CLI calls may be targeting remote while your local service is fine.
|
||||
- Explicit `--url` calls do not fall back to stored credentials.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `gateway connect failed:` → wrong URL target.
|
||||
- `unauthorized` → endpoint reachable but wrong auth.
|
||||
|
||||
### 2) Bind and auth guardrails are stricter
|
||||
|
||||
```bash
|
||||
openclaw config get gateway.bind
|
||||
openclaw config get gateway.auth.mode
|
||||
openclaw config get gateway.auth.token
|
||||
openclaw gateway status
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
What to check:
|
||||
|
||||
- Non-loopback binds (`lan`, `tailnet`, `custom`) need a valid gateway auth path: shared token/password auth, or a correctly configured non-loopback `trusted-proxy` deployment.
|
||||
- Old keys like `gateway.token` do not replace `gateway.auth.token`.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `refusing to bind gateway ... without auth` → non-loopback bind without a valid gateway auth path.
|
||||
- `Connectivity probe: failed` while runtime is running → gateway alive but inaccessible with current auth/url.
|
||||
|
||||
### 3) Pairing and device identity state changed
|
||||
|
||||
```bash
|
||||
openclaw devices list
|
||||
openclaw pairing list --channel <channel> [--account <id>]
|
||||
openclaw logs --follow
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
What to check:
|
||||
|
||||
- Pending device approvals for dashboard/nodes.
|
||||
- Pending DM pairing approvals after policy or identity changes.
|
||||
|
||||
Common signatures:
|
||||
|
||||
- `device identity required` → device auth not satisfied.
|
||||
- `pairing required` → sender/device must be approved.
|
||||
|
||||
If the service config and runtime still disagree after checks, reinstall service metadata from the same profile/state directory:
|
||||
|
||||
```bash
|
||||
openclaw gateway install --force
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
Related:
|
||||
|
||||
- [/gateway/pairing](/gateway/pairing)
|
||||
- [/gateway/authentication](/gateway/authentication)
|
||||
- [/gateway/background-process](/gateway/background-process)
|
||||
399
openclaw/docs/gateway/trusted-proxy-auth.md
Normal file
399
openclaw/docs/gateway/trusted-proxy-auth.md
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
---
|
||||
title: "Trusted Proxy Auth"
|
||||
summary: "Delegate gateway authentication to a trusted reverse proxy (Pomerium, Caddy, nginx + OAuth)"
|
||||
read_when:
|
||||
- Running OpenClaw behind an identity-aware proxy
|
||||
- Setting up Pomerium, Caddy, or nginx with OAuth in front of OpenClaw
|
||||
- Fixing WebSocket 1008 unauthorized errors with reverse proxy setups
|
||||
- Deciding where to set HSTS and other HTTP hardening headers
|
||||
---
|
||||
|
||||
# Trusted Proxy Auth
|
||||
|
||||
> ⚠️ **Security-sensitive feature.** This mode delegates authentication entirely to your reverse proxy. Misconfiguration can expose your Gateway to unauthorized access. Read this page carefully before enabling.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use `trusted-proxy` auth mode when:
|
||||
|
||||
- You run OpenClaw behind an **identity-aware proxy** (Pomerium, Caddy + OAuth, nginx + oauth2-proxy, Traefik + forward auth)
|
||||
- Your proxy handles all authentication and passes user identity via headers
|
||||
- You're in a Kubernetes or container environment where the proxy is the only path to the Gateway
|
||||
- You're hitting WebSocket `1008 unauthorized` errors because browsers can't pass tokens in WS payloads
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- If your proxy doesn't authenticate users (just a TLS terminator or load balancer)
|
||||
- If there's any path to the Gateway that bypasses the proxy (firewall holes, internal network access)
|
||||
- If you're unsure whether your proxy correctly strips/overwrites forwarded headers
|
||||
- If you only need personal single-user access (consider Tailscale Serve + loopback for simpler setup)
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Your reverse proxy authenticates users (OAuth, OIDC, SAML, etc.)
|
||||
2. Proxy adds a header with the authenticated user identity (e.g., `x-forwarded-user: nick@example.com`)
|
||||
3. OpenClaw checks that the request came from a **trusted proxy IP** (configured in `gateway.trustedProxies`)
|
||||
4. OpenClaw extracts the user identity from the configured header
|
||||
5. If everything checks out, the request is authorized
|
||||
|
||||
## Control UI Pairing Behavior
|
||||
|
||||
When `gateway.auth.mode = "trusted-proxy"` is active and the request passes
|
||||
trusted-proxy checks, Control UI WebSocket sessions can connect without device
|
||||
pairing identity.
|
||||
|
||||
Implications:
|
||||
|
||||
- Pairing is no longer the primary gate for Control UI access in this mode.
|
||||
- Your reverse proxy auth policy and `allowUsers` become the effective access control.
|
||||
- Keep gateway ingress locked to trusted proxy IPs only (`gateway.trustedProxies` + firewall).
|
||||
|
||||
## Configuration
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
// Trusted-proxy auth expects requests from a non-loopback trusted proxy source
|
||||
bind: "lan",
|
||||
|
||||
// CRITICAL: Only add your proxy's IP(s) here
|
||||
trustedProxies: ["10.0.0.1", "172.17.0.1"],
|
||||
|
||||
auth: {
|
||||
mode: "trusted-proxy",
|
||||
trustedProxy: {
|
||||
// Header containing authenticated user identity (required)
|
||||
userHeader: "x-forwarded-user",
|
||||
|
||||
// Optional: headers that MUST be present (proxy verification)
|
||||
requiredHeaders: ["x-forwarded-proto", "x-forwarded-host"],
|
||||
|
||||
// Optional: restrict to specific users (empty = allow all)
|
||||
allowUsers: ["nick@example.com", "admin@company.org"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Important runtime rule:
|
||||
|
||||
- Trusted-proxy auth rejects loopback-source requests (`127.0.0.1`, `::1`, loopback CIDRs).
|
||||
- Same-host loopback reverse proxies do **not** satisfy trusted-proxy auth.
|
||||
- For same-host loopback proxy setups, use token/password auth instead, or route through a non-loopback trusted proxy address that OpenClaw can verify.
|
||||
- Non-loopback Control UI deployments still need explicit `gateway.controlUi.allowedOrigins`.
|
||||
|
||||
### Configuration Reference
|
||||
|
||||
| Field | Required | Description |
|
||||
| ------------------------------------------- | -------- | --------------------------------------------------------------------------- |
|
||||
| `gateway.trustedProxies` | Yes | Array of proxy IP addresses to trust. Requests from other IPs are rejected. |
|
||||
| `gateway.auth.mode` | Yes | Must be `"trusted-proxy"` |
|
||||
| `gateway.auth.trustedProxy.userHeader` | Yes | Header name containing the authenticated user identity |
|
||||
| `gateway.auth.trustedProxy.requiredHeaders` | No | Additional headers that must be present for the request to be trusted |
|
||||
| `gateway.auth.trustedProxy.allowUsers` | No | Allowlist of user identities. Empty means allow all authenticated users. |
|
||||
|
||||
## TLS termination and HSTS
|
||||
|
||||
Use one TLS termination point and apply HSTS there.
|
||||
|
||||
### Recommended pattern: proxy TLS termination
|
||||
|
||||
When your reverse proxy handles HTTPS for `https://control.example.com`, set
|
||||
`Strict-Transport-Security` at the proxy for that domain.
|
||||
|
||||
- Good fit for internet-facing deployments.
|
||||
- Keeps certificate + HTTP hardening policy in one place.
|
||||
- OpenClaw can stay on loopback HTTP behind the proxy.
|
||||
|
||||
Example header value:
|
||||
|
||||
```text
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||
```
|
||||
|
||||
### Gateway TLS termination
|
||||
|
||||
If OpenClaw itself serves HTTPS directly (no TLS-terminating proxy), set:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
tls: { enabled: true },
|
||||
http: {
|
||||
securityHeaders: {
|
||||
strictTransportSecurity: "max-age=31536000; includeSubDomains",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`strictTransportSecurity` accepts a string header value, or `false` to disable explicitly.
|
||||
|
||||
### Rollout guidance
|
||||
|
||||
- Start with a short max age first (for example `max-age=300`) while validating traffic.
|
||||
- Increase to long-lived values (for example `max-age=31536000`) only after confidence is high.
|
||||
- Add `includeSubDomains` only if every subdomain is HTTPS-ready.
|
||||
- Use preload only if you intentionally meet preload requirements for your full domain set.
|
||||
- Loopback-only local development does not benefit from HSTS.
|
||||
|
||||
## Proxy Setup Examples
|
||||
|
||||
### Pomerium
|
||||
|
||||
Pomerium passes identity in `x-pomerium-claim-email` (or other claim headers) and a JWT in `x-pomerium-jwt-assertion`.
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
trustedProxies: ["10.0.0.1"], // Pomerium's IP
|
||||
auth: {
|
||||
mode: "trusted-proxy",
|
||||
trustedProxy: {
|
||||
userHeader: "x-pomerium-claim-email",
|
||||
requiredHeaders: ["x-pomerium-jwt-assertion"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Pomerium config snippet:
|
||||
|
||||
```yaml
|
||||
routes:
|
||||
- from: https://openclaw.example.com
|
||||
to: http://openclaw-gateway:18789
|
||||
policy:
|
||||
- allow:
|
||||
or:
|
||||
- email:
|
||||
is: nick@example.com
|
||||
pass_identity_headers: true
|
||||
```
|
||||
|
||||
### Caddy with OAuth
|
||||
|
||||
Caddy with the `caddy-security` plugin can authenticate users and pass identity headers.
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
trustedProxies: ["10.0.0.1"], // Caddy/sidecar proxy IP
|
||||
auth: {
|
||||
mode: "trusted-proxy",
|
||||
trustedProxy: {
|
||||
userHeader: "x-forwarded-user",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Caddyfile snippet:
|
||||
|
||||
```
|
||||
openclaw.example.com {
|
||||
authenticate with oauth2_provider
|
||||
authorize with policy1
|
||||
|
||||
reverse_proxy openclaw:18789 {
|
||||
header_up X-Forwarded-User {http.auth.user.email}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### nginx + oauth2-proxy
|
||||
|
||||
oauth2-proxy authenticates users and passes identity in `x-auth-request-email`.
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
trustedProxies: ["10.0.0.1"], // nginx/oauth2-proxy IP
|
||||
auth: {
|
||||
mode: "trusted-proxy",
|
||||
trustedProxy: {
|
||||
userHeader: "x-auth-request-email",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
nginx config snippet:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
auth_request /oauth2/auth;
|
||||
auth_request_set $user $upstream_http_x_auth_request_email;
|
||||
|
||||
proxy_pass http://openclaw:18789;
|
||||
proxy_set_header X-Auth-Request-Email $user;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
```
|
||||
|
||||
### Traefik with Forward Auth
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
trustedProxies: ["172.17.0.1"], // Traefik container IP
|
||||
auth: {
|
||||
mode: "trusted-proxy",
|
||||
trustedProxy: {
|
||||
userHeader: "x-forwarded-user",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Mixed token configuration
|
||||
|
||||
OpenClaw rejects ambiguous configurations where both a `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`) and `trusted-proxy` mode are active at the same time. Mixed token configs can cause loopback requests to silently authenticate on the wrong auth path.
|
||||
|
||||
If you see a `mixed_trusted_proxy_token` error on startup:
|
||||
|
||||
- Remove the shared token when using trusted-proxy mode, or
|
||||
- Switch `gateway.auth.mode` to `"token"` if you intend token-based auth.
|
||||
|
||||
Loopback trusted-proxy auth also fails closed: same-host callers must supply the configured identity headers through a trusted proxy instead of being silently authenticated.
|
||||
|
||||
## Operator scopes header
|
||||
|
||||
Trusted-proxy auth is an **identity-bearing** HTTP mode, so callers may
|
||||
optionally declare operator scopes with `x-openclaw-scopes`.
|
||||
|
||||
Examples:
|
||||
|
||||
- `x-openclaw-scopes: operator.read`
|
||||
- `x-openclaw-scopes: operator.read,operator.write`
|
||||
- `x-openclaw-scopes: operator.admin,operator.write`
|
||||
|
||||
Behavior:
|
||||
|
||||
- When the header is present, OpenClaw honors the declared scope set.
|
||||
- When the header is present but empty, the request declares **no** operator scopes.
|
||||
- When the header is absent, normal identity-bearing HTTP APIs fall back to the standard operator default scope set.
|
||||
- Gateway-auth **plugin HTTP routes** are narrower by default: when `x-openclaw-scopes` is absent, their runtime scope falls back to `operator.write`.
|
||||
- Browser-origin HTTP requests still have to pass `gateway.controlUi.allowedOrigins` (or deliberate Host-header fallback mode) even after trusted-proxy auth succeeds.
|
||||
|
||||
Practical rule:
|
||||
|
||||
- Send `x-openclaw-scopes` explicitly when you want a trusted-proxy request to
|
||||
be narrower than the defaults, or when a gateway-auth plugin route needs
|
||||
something stronger than write scope.
|
||||
|
||||
## Security Checklist
|
||||
|
||||
Before enabling trusted-proxy auth, verify:
|
||||
|
||||
- [ ] **Proxy is the only path**: The Gateway port is firewalled from everything except your proxy
|
||||
- [ ] **trustedProxies is minimal**: Only your actual proxy IPs, not entire subnets
|
||||
- [ ] **No loopback proxy source**: trusted-proxy auth fails closed for loopback-source requests
|
||||
- [ ] **Proxy strips headers**: Your proxy overwrites (not appends) `x-forwarded-*` headers from clients
|
||||
- [ ] **TLS termination**: Your proxy handles TLS; users connect via HTTPS
|
||||
- [ ] **allowedOrigins is explicit**: Non-loopback Control UI uses explicit `gateway.controlUi.allowedOrigins`
|
||||
- [ ] **allowUsers is set** (recommended): Restrict to known users rather than allowing anyone authenticated
|
||||
- [ ] **No mixed token config**: Do not set both `gateway.auth.token` and `gateway.auth.mode: "trusted-proxy"`
|
||||
|
||||
## Security Audit
|
||||
|
||||
`openclaw security audit` will flag trusted-proxy auth with a **critical** severity finding. This is intentional — it's a reminder that you're delegating security to your proxy setup.
|
||||
|
||||
The audit checks for:
|
||||
|
||||
- Base `gateway.trusted_proxy_auth` warning/critical reminder
|
||||
- Missing `trustedProxies` configuration
|
||||
- Missing `userHeader` configuration
|
||||
- Empty `allowUsers` (allows any authenticated user)
|
||||
- Wildcard or missing browser-origin policy on exposed Control UI surfaces
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "trusted_proxy_untrusted_source"
|
||||
|
||||
The request didn't come from an IP in `gateway.trustedProxies`. Check:
|
||||
|
||||
- Is the proxy IP correct? (Docker container IPs can change)
|
||||
- Is there a load balancer in front of your proxy?
|
||||
- Use `docker inspect` or `kubectl get pods -o wide` to find actual IPs
|
||||
|
||||
### "trusted_proxy_loopback_source"
|
||||
|
||||
OpenClaw rejected a loopback-source trusted-proxy request.
|
||||
|
||||
Check:
|
||||
|
||||
- Is the proxy connecting from `127.0.0.1` / `::1`?
|
||||
- Are you trying to use trusted-proxy auth with a same-host loopback reverse proxy?
|
||||
|
||||
Fix:
|
||||
|
||||
- Use token/password auth for same-host loopback proxy setups, or
|
||||
- Route through a non-loopback trusted proxy address and keep that IP in `gateway.trustedProxies`.
|
||||
|
||||
### "trusted_proxy_user_missing"
|
||||
|
||||
The user header was empty or missing. Check:
|
||||
|
||||
- Is your proxy configured to pass identity headers?
|
||||
- Is the header name correct? (case-insensitive, but spelling matters)
|
||||
- Is the user actually authenticated at the proxy?
|
||||
|
||||
### "trusted*proxy_missing_header*\*"
|
||||
|
||||
A required header wasn't present. Check:
|
||||
|
||||
- Your proxy configuration for those specific headers
|
||||
- Whether headers are being stripped somewhere in the chain
|
||||
|
||||
### "trusted_proxy_user_not_allowed"
|
||||
|
||||
The user is authenticated but not in `allowUsers`. Either add them or remove the allowlist.
|
||||
|
||||
### "trusted_proxy_origin_not_allowed"
|
||||
|
||||
Trusted-proxy auth succeeded, but the browser `Origin` header did not pass Control UI origin checks.
|
||||
|
||||
Check:
|
||||
|
||||
- `gateway.controlUi.allowedOrigins` includes the exact browser origin
|
||||
- You are not relying on wildcard origins unless you intentionally want allow-all behavior
|
||||
- If you intentionally use Host-header fallback mode, `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` is set deliberately
|
||||
|
||||
### WebSocket Still Failing
|
||||
|
||||
Make sure your proxy:
|
||||
|
||||
- Supports WebSocket upgrades (`Upgrade: websocket`, `Connection: upgrade`)
|
||||
- Passes the identity headers on WebSocket upgrade requests (not just HTTP)
|
||||
- Doesn't have a separate auth path for WebSocket connections
|
||||
|
||||
## Migration from Token Auth
|
||||
|
||||
If you're moving from token auth to trusted-proxy:
|
||||
|
||||
1. Configure your proxy to authenticate users and pass headers
|
||||
2. Test the proxy setup independently (curl with headers)
|
||||
3. Update OpenClaw config with trusted-proxy auth
|
||||
4. Restart the Gateway
|
||||
5. Test WebSocket connections from the Control UI
|
||||
6. Run `openclaw security audit` and review findings
|
||||
|
||||
## Related
|
||||
|
||||
- [Security](/gateway/security) — full security guide
|
||||
- [Configuration](/gateway/configuration) — config reference
|
||||
- [Remote Access](/gateway/remote) — other remote access patterns
|
||||
- [Tailscale](/gateway/tailscale) — simpler alternative for tailnet-only access
|
||||
Loading…
Add table
Add a link
Reference in a new issue