From bb5010b89a5a2b1dfe03e15e03f7d648ce21daa1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 22 May 2026 21:51:52 +0100 Subject: [PATCH] docs: absorb docs sweep Co-authored-by: Kai Co-authored-by: Weihang Co-authored-by: Scott Long Co-authored-by: moejaberr Co-authored-by: huihui0822 <109355071+huihui0822@users.noreply.github.com> --- CHANGELOG.md | 1 + apps/android/README.md | 7 ++- docs/.i18n/zh-Hans-navigation.json | 13 +++- docs/channels/zalouser.md | 18 ++++++ docs/cli/gateway.md | 7 ++- docs/gateway/protocol.md | 8 +++ docs/gateway/secrets.md | 88 ++++++++++++++++++++++++++++ docs/gateway/trusted-proxy-auth.md | 32 ++++++++++ docs/platforms/mac/peekaboo.md | 4 ++ docs/platforms/mac/permissions.md | 20 +++++++ skills/node-connect/SKILL.md | 3 +- src/commands/onboard-helpers.test.ts | 11 ++++ src/commands/onboard-helpers.ts | 2 + src/config/types.gateway.ts | 7 ++- src/gateway/control-ui-links.ts | 2 + 15 files changed, 214 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13e45695c70..b5cb36f3364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/apps/android/README.md b/apps/android/README.md index 9a6d07560f6..cc812f97697 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -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 ``` 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 `) 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`: diff --git a/docs/.i18n/zh-Hans-navigation.json b/docs/.i18n/zh-Hans-navigation.json index 641d4b0054c..9bc6e12f72c 100644 --- a/docs/.i18n/zh-Hans-navigation.json +++ b/docs/.i18n/zh-Hans-navigation.json @@ -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" ] }, diff --git a/docs/channels/zalouser.md b/docs/channels/zalouser.md index 1bbcaf1bae4..25ac94c2eac 100644 --- a/docs/channels/zalouser.md +++ b/docs/channels/zalouser.md @@ -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). diff --git a/docs/cli/gateway.md b/docs/cli/gateway.md index 29c43c40d9d..0e221fbbc3c 100644 --- a/docs/cli/gateway.md +++ b/docs/cli/gateway.md @@ -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`). - Listener bind mode. + Listener bind mode. `lan`, `tailnet`, and `custom` currently resolve over IPv4-only paths. Auth mode override. @@ -74,6 +76,9 @@ openclaw gateway run Reset Tailscale serve/funnel config on shutdown. + + 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. + 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. diff --git a/docs/gateway/protocol.md b/docs/gateway/protocol.md index f9dd5180d6f..bc4ef64e0f1 100644 --- a/docs/gateway/protocol.md +++ b/docs/gateway/protocol.md @@ -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 diff --git a/docs/gateway/secrets.md b/docs/gateway/secrets.md index 0ce6e8fbd7b..e88e5b94de4 100644 --- a/docs/gateway/secrets.md +++ b/docs/gateway/secrets.md @@ -339,6 +339,94 @@ the config fields that accept SecretRefs. } ``` + + 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 + ``` + + ```json5 { diff --git a/docs/gateway/trusted-proxy-auth.md b/docs/gateway/trusted-proxy-auth.md index 638880bbeaf..9cad73a3386 100644 --- a/docs/gateway/trusted-proxy-auth.md +++ b/docs/gateway/trusted-proxy-auth.md @@ -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. + + + 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. + Make sure your proxy: diff --git a/docs/platforms/mac/peekaboo.md b/docs/platforms/mac/peekaboo.md index f16704b6004..9a93a1d17d8 100644 --- a/docs/platforms/mac/peekaboo.md +++ b/docs/platforms/mac/peekaboo.md @@ -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. diff --git a/docs/platforms/mac/permissions.md b/docs/platforms/mac/permissions.md index 4ff7cdda253..72df50a0689 100644 --- a/docs/platforms/mac/permissions.md +++ b/docs/platforms/mac/permissions.md @@ -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. diff --git a/skills/node-connect/SKILL.md b/skills/node-connect/SKILL.md index 023380ab3ff..273cf722851 100644 --- a/skills/node-connect/SKILL.md +++ b/skills/node-connect/SKILL.md @@ -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 ``` If the app says `bootstrap token invalid or expired`: diff --git a/src/commands/onboard-helpers.test.ts b/src/commands/onboard-helpers.test.ts index 053d674f8c3..0967f33cd63 100644 --- a/src/commands/onboard-helpers.test.ts +++ b/src/commands/onboard-helpers.test.ts @@ -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({ diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 4171d3ba681..fc4d2b7c723 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -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", diff --git a/src/config/types.gateway.ts b/src/config/types.gateway.ts index 83cdde9ec48..b49dd42b9bb 100644 --- a/src/config/types.gateway.ts +++ b/src/config/types.gateway.ts @@ -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; diff --git a/src/gateway/control-ui-links.ts b/src/gateway/control-ui-links.ts index d39d2d9d502..2587f881c71 100644 --- a/src/gateway/control-ui-links.ts +++ b/src/gateway/control-ui-links.ts @@ -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();