diff --git a/CHANGELOG.md b/CHANGELOG.md index feaaa2c27de..2f1fce7921f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Packaging: exclude documentation images and assets from the npm tarball, reducing published package size without affecting runtime docs search or CLI behavior. Thanks @SebTardif. - Agents/subagents: limit default sub-agent bootstrap context to `AGENTS.md` and `TOOLS.md`, keeping persona, identity, user, memory, heartbeat, and setup files out of delegated workers by default. (#85283) Thanks @100yenadmin. - Maintainer skills: exclude plugin SDK/API boundary work from `openclaw-landable-bug-sweep` so bugbash sweeps stay focused on small paper-cut fixes. - Plugin SDK: add a generic channel-message poll sender so channel plugins can expose poll delivery without depending on channel-specific SDK facades. @@ -38,6 +39,7 @@ Docs: https://docs.openclaw.ai ### Fixes - CLI/update: repair managed npm plugin `openclaw` peer links during post-core convergence and reject stale or wrong-target peer links before restart. (#83794) Thanks @fuller-stack-dev. +- CLI/agents: default new omitted-account bindings to all accounts when the channel has multiple configured accounts, and clarify account-scope docs. (#49769) Thanks @Gcaufy. - Codex app-server: disable native Code Mode when the effective exec host is `node` and keep OpenClaw `exec`/`process` available, so `/exec host=node` routes shell commands through the selected node instead of the gateway. Fixes #85012. (#85090) Thanks @sahilsatralkar. - Agents: bound embedded auto-compaction session write-lock watchdogs to the compaction timeout instead of the full run timeout, so stuck compaction cannot hold the live session lock for the whole run window. (#84949) Thanks @luoyanglang. - Gateway: defer provider auth-state prewarm until after startup readiness so early gateway tool/session requests are not blocked by provider auth discovery. (#85272) Thanks @dutifulbob. diff --git a/docs/cli/agents.md b/docs/cli/agents.md index 5055c95982b..68ebab2c546 100644 --- a/docs/cli/agents.md +++ b/docs/cli/agents.md @@ -21,6 +21,7 @@ Related: openclaw agents list openclaw agents list --bindings openclaw agents add work --workspace ~/.openclaw/workspace-work +openclaw agents add work --workspace ~/.openclaw/workspace-work --bind telegram:* openclaw agents add ops --workspace ~/.openclaw/workspace-ops --bind telegram:ops --non-interactive openclaw agents bindings openclaw agents bind --agent work --bind telegram:ops @@ -50,27 +51,47 @@ Add bindings: openclaw agents bind --agent work --bind telegram:ops --bind discord:guild-a ``` -If you omit `accountId` (`--bind `), OpenClaw resolves it from channel defaults and plugin setup hooks when available. +You can also add bindings when creating an agent: + +```bash +openclaw agents add work --workspace ~/.openclaw/workspace-work --bind telegram:* --bind discord:* +``` + +If you omit `accountId` (`--bind `), OpenClaw resolves it from plugin setup hooks, forced account binding, or the channel's configured account count. If you omit `--agent` for `bind` or `unbind`, OpenClaw targets the current default agent. +### `--bind` format + +| Format | Meaning | +| ---------------------------- | ------------------------------------------------------------------------------------------------- | +| `--bind :*` | Match all accounts on the channel. | +| `--bind :` | Match one account. | +| `--bind ` | Match the default account only unless the CLI can safely resolve a plugin-specific account scope. | + ### Binding scope behavior -- A binding without `accountId` matches the channel default account only. +- A stored binding without `accountId` matches the channel default account only. - `accountId: "*"` is the channel-wide fallback (all accounts) and is less specific than an explicit account binding. - If the same agent already has a matching channel binding without `accountId`, and you later bind with an explicit or resolved `accountId`, OpenClaw upgrades that existing binding in place instead of adding a duplicate. -Example: +Examples: ```bash +# match all accounts on the channel +openclaw agents bind --agent work --bind telegram:* + +# match a specific account +openclaw agents bind --agent work --bind telegram:ops + # initial channel-only binding openclaw agents bind --agent work --bind telegram # later upgrade to account-scoped binding -openclaw agents bind --agent work --bind telegram:ops +openclaw agents bind --agent work --bind telegram:alerts ``` -After the upgrade, routing for that binding is scoped to `telegram:ops`. If you also want default-account routing, add it explicitly (for example `--bind telegram:default`). +After the upgrade, routing for that binding is scoped to `telegram:alerts`. If you also want default-account routing, add it explicitly (for example `--bind telegram:default`). Remove bindings: diff --git a/docs/cli/devices.md b/docs/cli/devices.md index ad3b8b26926..1d316835ea6 100644 --- a/docs/cli/devices.md +++ b/docs/cli/devices.md @@ -77,6 +77,36 @@ openclaw devices approve openclaw devices approve --latest ``` +## Paperclip / `openclaw_gateway` first-run approval + +When a new Paperclip agent connects through the `openclaw_gateway` adapter for the first time, the Gateway may require a one-time device pairing approval before runs can succeed. If Paperclip reports `openclaw_gateway_pairing_required`, approve the pending device and retry. + +For local gateways, preview the latest pending request: + +```bash +openclaw devices approve --latest +``` + +The preview prints the exact `openclaw devices approve ` command. Verify the request details, then rerun that command with the request ID to approve it. + +For remote gateways or explicit credentials, pass the same options while previewing and approving: + +```bash +openclaw devices approve --latest --url --token +``` + +To avoid re-approving after restarts, keep a persistent device key in the Paperclip adapter config instead of generating a new ephemeral identity each run: + +```json +{ + "adapterConfig": { + "devicePrivateKeyPem": "" + } +} +``` + +If approval keeps failing, run `openclaw devices list` first to confirm a pending request exists. + ### `openclaw devices reject ` Reject a pending device pairing request. diff --git a/docs/concepts/multi-agent.md b/docs/concepts/multi-agent.md index 32139af31ae..d9f102be14f 100644 --- a/docs/concepts/multi-agent.md +++ b/docs/concepts/multi-agent.md @@ -245,8 +245,9 @@ Bindings are **deterministic** and **most-specific wins**: - - A binding that omits `accountId` matches the default account only. + - A binding that omits `accountId` matches the default account only. It does not match all accounts. - Use `accountId: "*"` for a channel-wide fallback across all accounts. + - Use `accountId: ""` to match one account. - If you later add the same binding for the same agent with an explicit account id, OpenClaw upgrades the existing channel-only binding to account-scoped instead of duplicating it. @@ -457,15 +458,15 @@ Common channels supporting this pattern include: ], }, bindings: [ - { agentId: "chat", match: { channel: "whatsapp" } }, - { agentId: "opus", match: { channel: "telegram" } }, + { agentId: "chat", match: { channel: "whatsapp", accountId: "*" } }, + { agentId: "opus", match: { channel: "telegram", accountId: "*" } }, ], } ``` Notes: - - If you have multiple accounts for a channel, add `accountId` to the binding (for example `{ channel: "whatsapp", accountId: "personal" }`). + - These examples use `accountId: "*"` so the bindings keep working if you add accounts later. - To route a single DM/group to Opus while keeping the rest on chat, add a `match.peer` binding for that peer; peer matches always win over channel-wide rules. @@ -493,9 +494,9 @@ Common channels supporting this pattern include: bindings: [ { agentId: "opus", - match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551234567" } }, + match: { channel: "whatsapp", accountId: "*", peer: { kind: "direct", id: "+15551234567" } }, }, - { agentId: "chat", match: { channel: "whatsapp" } }, + { agentId: "chat", match: { channel: "whatsapp", accountId: "*" } }, ], } ``` diff --git a/docs/providers/claude-max-api-proxy.md b/docs/providers/claude-max-api-proxy.md index 57e5914506d..0f31f595c47 100644 --- a/docs/providers/claude-max-api-proxy.md +++ b/docs/providers/claude-max-api-proxy.md @@ -153,12 +153,6 @@ The proxy: -## Links - -- **npm:** [https://www.npmjs.com/package/claude-max-api-proxy](https://www.npmjs.com/package/claude-max-api-proxy) -- **GitHub:** [https://github.com/atalovesyou/claude-max-api-proxy](https://github.com/atalovesyou/claude-max-api-proxy) -- **Issues:** [https://github.com/atalovesyou/claude-max-api-proxy/issues](https://github.com/atalovesyou/claude-max-api-proxy/issues) - ## Notes - This is a **community tool**, not officially supported by Anthropic or OpenClaw diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 831d4e4d987..6e6e4e2e7f9 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -56,6 +56,8 @@ If the browser is already paired and you change it from read access to write/adm Once approved, the device is remembered and won't require re-approval unless you revoke it with `openclaw devices revoke --device --role `. See [Devices CLI](/cli/devices) for token rotation and revocation. +Paperclip agents that connect through the `openclaw_gateway` adapter use the same first-run approval flow. After the initial connection attempt, run `openclaw devices approve --latest` to preview the pending request, then rerun the printed `openclaw devices approve ` command to approve it. Pass explicit `--url` and `--token` values for a remote gateway. To keep approvals stable across restarts, configure a persistent `adapterConfig.devicePrivateKeyPem` in Paperclip instead of letting it generate a new ephemeral device identity each run. + - Direct local loopback browser connections (`127.0.0.1` / `localhost`) are auto-approved. - Tailscale Serve can skip the pairing round trip for Control UI operator sessions when `gateway.auth.allowTailscale: true`, Tailscale identity verifies, and the browser presents its device identity. diff --git a/package.json b/package.json index f4a51b1453b..a51b60bc172 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,10 @@ "docs/", "!docs/.generated/**", "!docs/channels/qa-channel.md", + "!docs/assets/**", + "!docs/images/**", + "!docs/**/*.jpg", + "!docs/**/*.png", "scripts/crabbox-wrapper.mjs", "patches/", "skills/", diff --git a/scripts/docs-i18n/localized_links.go b/scripts/docs-i18n/localized_links.go index d7845c4451a..ebe1e523666 100644 --- a/scripts/docs-i18n/localized_links.go +++ b/scripts/docs-i18n/localized_links.go @@ -325,17 +325,24 @@ func (ri *routeIndex) localizeURL(raw string) string { func hasURLScheme(raw string) bool { switch { - case strings.HasPrefix(raw, "http://"), strings.HasPrefix(raw, "https://"): + case hasSchemePrefix(raw, "http://"), hasSchemePrefix(raw, "https://"): return true - case strings.HasPrefix(raw, "mailto:"), strings.HasPrefix(raw, "tel:"): + case hasSchemePrefix(raw, "mailto:"), hasSchemePrefix(raw, "tel:"): return true - case strings.HasPrefix(raw, "data:"), strings.HasPrefix(raw, "javascript:"): + case hasSchemePrefix(raw, "data:"), hasSchemePrefix(raw, "javascript:"), hasSchemePrefix(raw, "vbscript:"): return true default: return false } } +func hasSchemePrefix(raw, prefix string) bool { + if len(raw) < len(prefix) { + return false + } + return strings.EqualFold(raw[:len(prefix)], prefix) +} + func splitURLSuffix(raw string) (string, string) { index := strings.IndexAny(raw, "?#") if index == -1 { diff --git a/scripts/docs-i18n/localized_links_test.go b/scripts/docs-i18n/localized_links_test.go index 0e50d934d20..6d2bae75b42 100644 --- a/scripts/docs-i18n/localized_links_test.go +++ b/scripts/docs-i18n/localized_links_test.go @@ -49,6 +49,16 @@ func TestLocalizeBodyLinks(t *testing.T) { input: `See [Config](/zh-CN/gateway/configuration).`, want: `See [Config](/zh-CN/gateway/configuration).`, }, + { + name: "vbscript scheme stays unchanged", + input: `bad`, + want: `bad`, + }, + { + name: "mixed-case javascript scheme stays unchanged", + input: `bad`, + want: `bad`, + }, { name: "missing localized page stays unchanged", input: `See [FAQ](/help/faq).`, diff --git a/src/commands/agents.bind.commands.test.ts b/src/commands/agents.bind.commands.test.ts index 5dc4682bc62..58bb5430937 100644 --- a/src/commands/agents.bind.commands.test.ts +++ b/src/commands/agents.bind.commands.test.ts @@ -47,6 +47,7 @@ function createBindingResolverTestPlugin(params: { id: ChannelId; config: Partial; resolveBindingAccountId?: NonNullable["resolveBindingAccountId"]; + forceAccountBinding?: boolean; }): BindingResolverTestPlugin { return { id: params.id, @@ -56,6 +57,7 @@ function createBindingResolverTestPlugin(params: { selectionLabel: params.id, docsPath: `/channels/${params.id}`, blurb: "test stub.", + ...(params.forceAccountBinding ? { forceAccountBinding: true } : {}), }, capabilities: { chatTypes: ["direct"] }, config: { @@ -93,6 +95,14 @@ vi.mock("../channels/plugins/bundled.js", () => { "telegram", createBindingResolverTestPlugin({ id: "telegram", config: { listAccountIds: () => [] } }), ], + [ + "whatsapp", + createBindingResolverTestPlugin({ + id: "whatsapp", + config: { listAccountIds: () => ["default", "biz"] }, + forceAccountBinding: true, + }), + ], ]); return { getBundledChannelSetupPlugin: (channel: string) => { @@ -160,6 +170,22 @@ describe("agents bind/unbind commands", () => { expect(runtime.exit).not.toHaveBeenCalled(); }); + it("uses a wildcard account binding for multi-account channels", async () => { + readConfigFileSnapshotMock.mockResolvedValue({ + ...baseConfigSnapshot, + config: {}, + }); + + await agentsBindCommand({ bind: ["whatsapp"] }, runtime); + + expect(writeConfigFileMock).toHaveBeenCalledTimes(1); + const writtenConfig = firstWrittenConfig(); + expect(writtenConfig?.bindings).toStrictEqual([ + { type: "route", agentId: "main", match: { channel: "whatsapp", accountId: "*" } }, + ]); + expect(runtime.exit).not.toHaveBeenCalled(); + }); + it("binds manifest-known external channels without loading plugin runtime", async () => { readConfigFileSnapshotMock.mockResolvedValue({ ...baseConfigSnapshot, diff --git a/src/commands/agents.bindings.ts b/src/commands/agents.bindings.ts index af866f33716..7eb1f4ea7f2 100644 --- a/src/commands/agents.bindings.ts +++ b/src/commands/agents.bindings.ts @@ -266,6 +266,10 @@ function resolveBindingAccountId(params: { return pluginAccountId.trim(); } + if (plugin && plugin.config.listAccountIds(params.config).length > 1) { + return "*"; + } + if (plugin?.meta.forceAccountBinding) { return resolveDefaultAccountId(params.config, params.channel); } diff --git a/src/config/types.agents.ts b/src/config/types.agents.ts index 59fb06d868b..b3d6dedacc8 100644 --- a/src/config/types.agents.ts +++ b/src/config/types.agents.ts @@ -40,6 +40,12 @@ export type AgentRuntimeConfig = export type AgentBindingMatch = { channel: string; + /** + * Channel account to match. + * - Omitted/empty: matches only the channel default account. + * - "*": matches every account on the channel. + * - Any other string: matches that specific account id. + */ accountId?: string; peer?: { kind: ChatType; id: string }; guildId?: string;