docs: absorb docs sweep

Co-authored-by: Kai <kai@itskai.dev>
Co-authored-by: Weihang <gwh7078@163.com>
Co-authored-by: Scott Long <longstoryscott@gmail.com>
Co-authored-by: moejaberr <mjaber@uoguelph.ca>
Co-authored-by: huihui0822 <109355071+huihui0822@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-05-22 21:51:52 +01:00
parent 60e3749de3
commit bb5010b89a
15 changed files with 214 additions and 9 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Docs: clarify IPv4-only Gateway BYOH binding, trusted-proxy scope clearing, Android pairing approval, macOS Accessibility grants, Zalo profile env vars, password-store SecretRef setup, and Chinese memory navigation. Thanks @itskai-dev, @gwh7078, @longstoryscott, @MoeJaberr, and @yuaiccc.
- Docs: consolidate GLM under Z.AI, add the Upstash Box install guide and Gateway exposure runbook, clarify MEDIA directives, Copilot and Voyage setup, config path quoting, real behavior proof, and memory-file write guidance. Thanks @BobDu, @alitariksahin, @Jefsky, @musaabhasan, @OmerZeyveli, @leno23, @WuKongAI-CMU, @luoyanglang, and @majin1102.
- Docs: clarify media provider credentials, Codex/OpenClaw code-mode boundaries, Slack and Telegram ack reactions, Feishu dynamic agents, secrets plaintext boundaries, memory guidance, and Chinese glossary terms. Thanks @nielskaspers, @cosmopolitan033, @drclaw-iq, @alexgduarte, @zccyman, @chengoak, and @cassthebandit.
- Packaging: exclude documentation images and assets from the npm tarball, reducing published package size without affecting runtime docs search or CLI behavior. Thanks @SebTardif.

View File

@@ -253,12 +253,13 @@ Pre-req checklist:
5) Grant runtime permissions for capabilities you expect to pass (camera/mic/location/notification listener/location, etc.).
6) No interactive system dialogs should be pending before test start.
7) Canvas host is enabled and reachable from the device (do not run gateway with `OPENCLAW_SKIP_CANVAS_HOST=1`; startup logs should include `canvas host mounted at .../__openclaw__/`).
8) Local operator test client pairing is approved. If first run fails with `pairing required`, approve latest pending device pairing request, then rerun:
8) Local operator test client pairing is approved. If first run fails with `pairing required`, preview the latest pending request, approve the printed request ID, then rerun:
9) For A2UI checks, keep the app on **Screen** tab; the node now auto-refreshes canvas capability once on first A2UI reachability failure (TTL-safe retry).
```bash
openclaw devices list
openclaw devices approve --latest
openclaw devices approve --latest # preview only; copy the requestId from output
openclaw devices approve <requestId>
```
Run:
@@ -284,7 +285,7 @@ What it does:
Common failure quick-fixes:
- `pairing required` before tests start:
- approve pending device pairing (`openclaw devices approve --latest`) and rerun.
- list pending requests (`openclaw devices list`), then approve with the exact ID (`openclaw devices approve <requestId>`) and rerun.
- `A2UI host not reachable` / `A2UI_HOST_NOT_CONFIGURED`:
- ensure the Canvas plugin host is running and reachable, keep the app on the **Screen** tab. The app refreshes the Canvas plugin surface URL once before failing; if it still fails, reconnect app and rerun.
- `NODE_BACKGROUND_UNAVAILABLE: canvas unavailable`:

View File

@@ -137,7 +137,18 @@
"zh-CN/concepts/session",
"zh-CN/concepts/session-pruning",
"zh-CN/concepts/session-tool",
"zh-CN/concepts/memory",
{
"group": "记忆",
"pages": [
"zh-CN/concepts/memory",
"zh-CN/concepts/memory-builtin",
"zh-CN/concepts/memory-qmd",
"zh-CN/concepts/memory-honcho",
"zh-CN/concepts/memory-search",
"zh-CN/concepts/active-memory",
"zh-CN/concepts/dreaming"
]
},
"zh-CN/concepts/compaction"
]
},

View File

@@ -166,6 +166,24 @@ Accounts map to `zalouser` profiles in OpenClaw state. Example:
}
```
## Environment variables
The Zalo Personal plugin can also read profile selection from environment variables:
- `ZALOUSER_PROFILE`: profile name to use when no `profile` is set in channel or account config.
- `ZCA_PROFILE`: legacy fallback profile name, used only when `ZALOUSER_PROFILE` is not set.
Profile names select the saved Zalo login credentials in OpenClaw state. Resolution order is:
1. Explicit `profile` in config.
2. `ZALOUSER_PROFILE`.
3. `ZCA_PROFILE`.
4. The account id for non-default accounts, or `default` for the default account.
For multi-account setups, prefer setting `profile` on each account in config so
one environment variable does not make multiple accounts share the same login
session.
## Typing, reactions, and delivery acknowledgements
- OpenClaw sends a typing event before dispatching a reply (best-effort).

View File

@@ -42,6 +42,8 @@ openclaw gateway run
- `openclaw onboard --mode local` and `openclaw setup` are expected to write `gateway.mode=local`. If the file exists but `gateway.mode` is missing, treat that as a broken or clobbered config and repair it instead of assuming local mode implicitly.
- If the file exists and `gateway.mode` is missing, the Gateway treats that as suspicious config damage and refuses to "guess local" for you.
- Binding beyond loopback without auth is blocked (safety guardrail).
- `lan`, `tailnet`, and `custom` currently resolve over IPv4-only BYOH paths.
- IPv6-only BYOH is not natively supported on this path today. Use an IPv4 sidecar or proxy if the host itself is IPv6-only.
- `SIGUSR1` triggers an in-process restart when authorized (`commands.restart` is enabled by default; set `commands.restart: false` to block manual restart, while gateway tool/config apply/update remain allowed).
- `SIGINT`/`SIGTERM` handlers stop the gateway process, but they don't restore any custom terminal state. If you wrap the CLI with a TUI or raw-mode input, restore the terminal before exit.
@@ -54,7 +56,7 @@ openclaw gateway run
WebSocket port (default comes from config/env; usually `18789`).
</ParamField>
<ParamField path="--bind <loopback|lan|tailnet|auto|custom>" type="string">
Listener bind mode.
Listener bind mode. `lan`, `tailnet`, and `custom` currently resolve over IPv4-only paths.
</ParamField>
<ParamField path="--auth <token|password>" type="string">
Auth mode override.
@@ -74,6 +76,9 @@ openclaw gateway run
<ParamField path="--tailscale-reset-on-exit" type="boolean">
Reset Tailscale serve/funnel config on shutdown.
</ParamField>
<ParamField path="--bind custom + gateway.customBindHost" type="string">
Expects an IPv4 address today. For IPv6-only BYOH, place an IPv4 sidecar or proxy in front of the Gateway and point OpenClaw at that IPv4 endpoint.
</ParamField>
<ParamField path="--allow-unconfigured" type="boolean">
Allow gateway start without `gateway.mode=local` in config. Bypasses the startup guard for ad-hoc/dev bootstrap only; does not write or repair the config file.
</ParamField>

View File

@@ -757,6 +757,14 @@ rather than the pre-handshake defaults.
- `gateway.controlUi.dangerouslyDisableDeviceAuth=true` (break-glass, severe security downgrade).
- direct-loopback `gateway-client` backend RPCs authenticated with the shared
gateway token/password.
- Omitting device identity has scope consequences. When a Control UI connection
lacks device identity, `shouldClearUnboundScopesForMissingDeviceIdentity`
clears self-declared scopes to an empty set for token, password, and
trusted-proxy auth. The connection is allowed on explicit trust paths, but
scope-gated methods fail. The exception is local Control UI token/password
sessions with `allowInsecureAuth`, which preserve scopes. For other cases,
set `gateway.controlUi.dangerouslyDisableDeviceAuth=true` only as a
break-glass scope-preservation path.
- All connections must sign the server-provided `connect.challenge` nonce.
### Device auth migration diagnostics

View File

@@ -339,6 +339,94 @@ the config fields that accept SecretRefs.
}
```
</Accordion>
<Accordion title="password-store (`pass`)">
Use a small resolver wrapper when you want SecretRef ids to map directly to
`pass` entries. Save this as an executable in an absolute path that passes
your exec-provider path checks, for example
`/usr/local/bin/openclaw-pass-resolver`. The `#!/usr/bin/env node` shebang
resolves `node` from the resolver process `PATH`, so include `PATH` in
`passEnv`. If `pass` is not on that `PATH`, set `PASS_BIN` in the parent
environment and include it in `passEnv` too:
```js
#!/usr/bin/env node
const { spawnSync } = require("node:child_process");
let stdin = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
stdin += chunk;
});
process.stdin.on("error", (err) => {
process.stderr.write(`${err.message}\n`);
process.exit(1);
});
process.stdin.on("end", () => {
let request;
try {
request = JSON.parse(stdin || "{}");
} catch (err) {
process.stderr.write(`Failed to parse request: ${err.message}\n`);
process.exit(1);
}
const passBin = process.env.PASS_BIN || "pass";
const values = {};
const errors = {};
for (const id of request.ids ?? []) {
const result = spawnSync(passBin, ["show", id], { encoding: "utf8" });
if (result.status === 0) {
values[id] = result.stdout.split(/\r?\n/, 1)[0] ?? "";
} else {
errors[id] = { message: (result.stderr || `pass exited ${result.status}`).trim() };
}
}
process.stdout.write(JSON.stringify({ protocolVersion: 1, values, errors }));
});
```
Then configure the exec provider and point `apiKey` at the `pass` entry path:
```json5
{
secrets: {
providers: {
pass_store: {
source: "exec",
command: "/usr/local/bin/openclaw-pass-resolver",
passEnv: ["PATH", "HOME", "GNUPGHOME", "GPG_TTY", "PASSWORD_STORE_DIR", "PASS_BIN"],
jsonOnly: true,
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [{ id: "gpt-5", name: "gpt-5" }],
apiKey: {
source: "exec",
provider: "pass_store",
id: "openclaw/providers/openai/apiKey",
},
},
},
},
}
```
Keep the secret on the first line of the `pass` entry, or customize the
wrapper if you want to return the full `pass show` output instead. After
updating config, verify both the static audit and the exec resolver path:
```bash
openclaw secrets audit --check
openclaw secrets audit --allow-exec
```
</Accordion>
<Accordion title="sops">
```json5
{

View File

@@ -59,6 +59,19 @@ Implications:
- Your reverse proxy auth policy and `allowUsers` become the effective access control.
- Keep gateway ingress locked to trusted proxy IPs only (`gateway.trustedProxies` + firewall).
**Scope clearing without device identity:** Because the browser over plain HTTP
cannot create the device identity that OpenClaw uses to bind operator scopes,
trusted-proxy WebSocket connections that lack device identity have their
self-declared scopes cleared to an empty set. The connection is allowed, but
scope-gated methods (`operator.read`, `operator.write`, etc.) fail with
`missing scope`.
To preserve operator scopes on trusted-proxy WebSocket connections without
device identity, set `gateway.controlUi.dangerouslyDisableDeviceAuth: true`.
This is a break-glass flag (`openclaw security audit` reports it as critical).
Use it only when the reverse proxy is the sole path to the Gateway and device
identity cannot be established.
## Configuration
```json5
@@ -311,6 +324,11 @@ Loopback trusted-proxy identity headers still fail closed: same-host callers are
Trusted-proxy auth is an **identity-bearing** HTTP mode, so callers may optionally declare operator scopes with `x-openclaw-scopes`.
Note: `x-openclaw-scopes` applies to HTTP endpoints only. WebSocket scopes are
determined by the Gateway protocol handshake and device identity binding. For
WebSocket scope behavior with trusted-proxy, see
[Control UI pairing behavior](#control-ui-pairing-behavior).
Examples:
- `x-openclaw-scopes: operator.read`
@@ -407,6 +425,20 @@ The audit checks for:
- 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.
</Accordion>
<Accordion title="Connection succeeds but methods report missing scope">
The WebSocket connects, but `chat.history` or `sessions.list` fails with
`missing scope: operator.read`.
This is expected for trusted-proxy WebSocket connections without device
identity. Connections lacking device identity have their scopes cleared. The
browser cannot generate device identity over plain HTTP.
Fix:
- Set `gateway.controlUi.dangerouslyDisableDeviceAuth: true` to preserve operator scopes on trusted-proxy WebSocket connections, or
- Use device identity pairing so scopes are bound to the device token.
</Accordion>
<Accordion title="WebSocket still failing">
Make sure your proxy:

View File

@@ -69,6 +69,10 @@ export PEEKABOO_BRIDGE_SOCKET=/path/to/bridge.sock
- The bridge validates **caller code signatures**; an allowlist of TeamIDs is
enforced (Peekaboo host TeamID + OpenClaw app TeamID).
- Prefer the signed bridge/app identity over a generic `node` runtime for
Accessibility. Granting Accessibility to `node` lets any package launched by
that Node executable inherit GUI automation access; see
[macOS permissions](/platforms/mac/permissions#accessibility-grants-for-node-and-cli-runtimes).
- Requests time out after ~10 seconds.
- If required permissions are missing, the bridge returns a clear error message
rather than launching System Settings.

View File

@@ -2,6 +2,7 @@
summary: "macOS permission persistence (TCC) and signing requirements"
read_when:
- Debugging missing or stuck macOS permission prompts
- Deciding whether to grant Accessibility to node or a CLI runtime
- Packaging or signing the macOS app
- Changing bundle IDs or app install paths
title: "macOS permissions"
@@ -22,6 +23,25 @@ macOS treats the app as new and may drop or hide prompts.
Ad-hoc signatures generate a new identity every build. macOS will forget previous
grants, and prompts can disappear entirely until the stale entries are cleared.
## Accessibility grants for Node and CLI runtimes
Prefer granting Accessibility to OpenClaw.app, Peekaboo.app, or another signed
helper with its own bundle identifier instead of a generic `node` binary.
macOS TCC grants Accessibility to the code identity of the process it sees. If a
Homebrew, nvm, pnpm, or npm workflow causes a shared `node` executable to
receive Accessibility, any JavaScript package launched through that same
executable may inherit GUI automation privileges.
Treat a `node` entry in System Settings as broad permission for that Node
runtime, not as permission for one npm package. Avoid granting Accessibility to
`node` unless you trust every script and package launched through that exact
Node install.
If you accidentally granted Accessibility to `node`, remove that entry from
System Settings -> Privacy & Security -> Accessibility. Then grant the signed
app or helper that should own UI automation.
## Recovery checklist when prompts disappear
1. Quit the app.

View File

@@ -104,7 +104,8 @@ If the app says `pairing required`:
```bash
openclaw devices list
openclaw devices approve --latest
openclaw devices approve --latest # preview only; copy the requestId from output
openclaw devices approve <requestId>
```
If the app says `bootstrap token invalid or expired`:

View File

@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../runtime.js";
import { withMockedPlatform } from "../test-utils/vitest-spies.js";
import {
formatControlUiSshHint,
handleReset,
normalizeGatewayTokenInput,
openUrl,
@@ -150,6 +151,16 @@ describe("resolveBrowserOpenCommand", () => {
});
});
describe("formatControlUiSshHint", () => {
it("includes the IPv4-only BYOH note and workaround", () => {
const hint = formatControlUiSshHint({ port: 18789 });
expect(hint).toContain("BYOH note: lan, tailnet, and custom bind are currently IPv4-only.");
expect(hint).toContain(
"If your host is IPv6-only, use an IPv4 sidecar or proxy in front of the Gateway.",
);
});
});
describe("probeGatewayReachable", () => {
it("uses a hello-only probe for onboarding reachability", async () => {
mocks.probeGateway.mockResolvedValueOnce({

View File

@@ -199,6 +199,8 @@ export function formatControlUiSshHint(params: {
"Then open:",
localUrl,
authedUrl,
"BYOH note: lan, tailnet, and custom bind are currently IPv4-only.",
"If your host is IPv6-only, use an IPv4 sidecar or proxy in front of the Gateway.",
"Docs:",
"https://docs.openclaw.ai/gateway/remote",
"https://docs.openclaw.ai/web/control-ui",

View File

@@ -456,14 +456,15 @@ export type GatewayConfig = {
/**
* Bind address policy for the Gateway WebSocket + Control UI HTTP server.
* - auto: Loopback (127.0.0.1) if available, else 0.0.0.0 (fallback to all interfaces)
* - lan: 0.0.0.0 (all interfaces, no fallback)
* - lan: 0.0.0.0 (all interfaces, no fallback, current BYOH path is IPv4-only)
* - loopback: 127.0.0.1 (local-only)
* - tailnet: Tailnet IPv4 if available (100.64.0.0/10), else loopback
* - custom: User-specified IP, fallback to 0.0.0.0 if unavailable (requires customBindHost)
* - custom: User-specified IPv4 address, fallback to 0.0.0.0 if unavailable (requires customBindHost)
* IPv6-only BYOH is not natively supported on this path today. Use an IPv4 sidecar or proxy.
* Default: loopback (127.0.0.1).
*/
bind?: GatewayBindMode;
/** Custom IP address for bind="custom" mode. Fallback: 0.0.0.0. */
/** Custom IPv4 address for bind="custom" mode. IPv6-only BYOH requires an IPv4 sidecar or proxy. */
customBindHost?: string;
controlUi?: GatewayControlUiConfig;
auth?: GatewayAuthConfig;

View File

@@ -12,6 +12,8 @@ export function resolveControlUiLinks(params: {
basePath?: string;
tlsEnabled?: boolean;
}): { httpUrl: string; wsUrl: string } {
// Current BYOH truth: lan, tailnet, and custom bind resolve through IPv4-only helpers.
// IPv6-only hosts need an IPv4 sidecar or proxy in front of the Gateway.
const port = params.port;
const bind = params.bind ?? "loopback";
const customBindHost = params.customBindHost?.trim();