fix: apply docs sweep updates

This commit is contained in:
Peter Steinberger
2026-05-22 18:39:08 +01:00
parent 769fd0b14a
commit 59aef2ff0d
12 changed files with 127 additions and 20 deletions

View File

@@ -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.

View File

@@ -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 <channel>`), 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 <channel>`), 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 <channel>:*` | Match all accounts on the channel. |
| `--bind <channel>:<account>` | Match one account. |
| `--bind <channel>` | 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:

View File

@@ -77,6 +77,36 @@ openclaw devices approve <requestId>
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 <requestId>` 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 <gateway-ws-url> --token <gateway-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": "<ed25519-private-key-pkcs8-pem>"
}
}
```
If approval keeps failing, run `openclaw devices list` first to confirm a pending request exists.
### `openclaw devices reject <requestId>`
Reject a pending device pairing request.

View File

@@ -245,8 +245,9 @@ Bindings are **deterministic** and **most-specific wins**:
</Accordion>
<Accordion title="Account-scope detail">
- 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: "<name>"` 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.
</Accordion>
@@ -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.
</Tab>
@@ -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: "*" } },
],
}
```

View File

@@ -153,12 +153,6 @@ The proxy:
</Accordion>
</AccordionGroup>
## 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

View File

@@ -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 <id> --role <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 <requestId>` 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.
<Note>
- 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.

View File

@@ -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/",

View File

@@ -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 {

View File

@@ -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: `<a href="vbscript:msgbox(1)">bad</a>`,
want: `<a href="vbscript:msgbox(1)">bad</a>`,
},
{
name: "mixed-case javascript scheme stays unchanged",
input: `<a href="Javascript:alert(1)">bad</a>`,
want: `<a href="Javascript:alert(1)">bad</a>`,
},
{
name: "missing localized page stays unchanged",
input: `See [FAQ](/help/faq).`,

View File

@@ -47,6 +47,7 @@ function createBindingResolverTestPlugin(params: {
id: ChannelId;
config: Partial<ChannelPlugin["config"]>;
resolveBindingAccountId?: NonNullable<ChannelPlugin["setup"]>["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,

View File

@@ -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);
}

View File

@@ -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;