Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
233
docs/gateway/pairing.md
Normal file
233
docs/gateway/pairing.md
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
In Gateway-owned pairing, the **Gateway** is the source of truth for which
|
||||
nodes may 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, legacy 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 rotate 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 remove --node <id|name|ip>
|
||||
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, or
|
||||
expired.
|
||||
|
||||
Methods:
|
||||
|
||||
- `node.pair.request` - create or reuse a pending request.
|
||||
- `node.pair.list` - list pending and paired nodes (`operator.pairing`).
|
||||
- `node.pair.approve` - approve a pending request (issues a token).
|
||||
- `node.pair.reject` - reject a pending request.
|
||||
- `node.pair.remove` - remove a paired node. For a device-backed pairing, this
|
||||
revokes the device's `node` role: it mutates `devices/paired.json` and
|
||||
invalidates/disconnects that device's node-role sessions. A **mixed-role**
|
||||
device (for example one that also holds `operator`) keeps its row and only
|
||||
loses the `node` role; a node-only device row is deleted. It also clears any
|
||||
matching legacy gateway-owned node pairing entry. Authz: `operator.pairing`
|
||||
may remove non-operator node rows; a device-token caller revoking its
|
||||
**own** node role on a mixed-role device additionally needs
|
||||
`operator.admin`.
|
||||
- `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 refresh the stored node
|
||||
metadata and the latest allowlisted declared command snapshot for operator
|
||||
visibility.
|
||||
- Approval **always** generates a fresh token; `node.pair.request` never
|
||||
returns a token.
|
||||
- Operator scope levels and approval-time checks are summarized in
|
||||
[Operator scopes](/gateway/operator-scopes).
|
||||
- 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`
|
||||
|
||||
<Warning>
|
||||
Node pairing is a trust and 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, filtered by
|
||||
the gateway's global node command policy (`gateway.nodes.allowCommands` and
|
||||
`denyCommands`).
|
||||
- Per-node `system.run` allow and ask policy lives on the node in
|
||||
`exec.approvals.node.*`, not in the pairing record.
|
||||
|
||||
</Warning>
|
||||
|
||||
## 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 that request is approved, all pending node commands from that node are
|
||||
filtered and will not execute. Once pairing is approved, the node's declared
|
||||
commands become available, subject to the normal command policy.
|
||||
|
||||
This means:
|
||||
|
||||
- Nodes that previously relied on device pairing alone to expose commands must
|
||||
now also 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 keeps node events from escalating into host-level tool access
|
||||
beyond what the node's trust boundary permits.
|
||||
|
||||
Durable node presence updates follow the same identity boundary: the
|
||||
`node.presence.alive` event is accepted only from authenticated node device
|
||||
sessions, and updates pairing metadata only when the device/node identity is
|
||||
already paired. A self-declared `client.id` value is not enough to write
|
||||
last-seen state.
|
||||
|
||||
## Auto-approval (macOS app)
|
||||
|
||||
The macOS app can 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.
|
||||
|
||||
## Trusted-CIDR device auto-approval
|
||||
|
||||
WS device pairing for `role: node` stays manual by default. For private node
|
||||
networks where the Gateway already trusts the network path, operators can opt
|
||||
in with explicit CIDRs or exact IPs:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
nodes: {
|
||||
pairing: {
|
||||
autoApproveCidrs: ["192.168.1.0/24"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Security boundary:
|
||||
|
||||
- Disabled when `gateway.nodes.pairing.autoApproveCidrs` is unset.
|
||||
- No blanket LAN or private-network auto-approve mode exists.
|
||||
- Only a fresh `role: node` device pairing request with no requested scopes is
|
||||
eligible.
|
||||
- Operator, browser, Control UI, and WebChat clients stay manual.
|
||||
- Role, scope, metadata, and public-key upgrades stay manual.
|
||||
- Same-host loopback trusted-proxy header paths are not eligible, because that
|
||||
path can be spoofed by local callers.
|
||||
|
||||
## Metadata-upgrade auto-approval
|
||||
|
||||
When an already-paired device reconnects with only non-sensitive metadata
|
||||
changes (for example display name or client platform hints), OpenClaw treats
|
||||
that as a `metadata-upgrade`. Silent auto-approval is narrow: it applies only
|
||||
to trusted non-browser local reconnects that already proved possession of
|
||||
local or shared credentials, including same-host native app reconnects after
|
||||
OS version metadata changes. Browser/Control UI clients and remote clients
|
||||
still use the explicit re-approval flow. Scope upgrades (read to
|
||||
write/admin) and public key changes are **not** eligible for
|
||||
metadata-upgrade auto-approval; they stay explicit re-approval requests.
|
||||
|
||||
## QR pairing helpers
|
||||
|
||||
`/pair qr` renders the pairing payload as structured media so mobile and
|
||||
browser clients can scan it directly.
|
||||
|
||||
Deleting a device also sweeps any stale pending pairing requests for that
|
||||
device id, so `nodes pending` does not show orphaned rows after a revoke.
|
||||
|
||||
## Locality and forwarded headers
|
||||
|
||||
Gateway pairing treats a connection as loopback only when both the raw socket
|
||||
and any upstream proxy evidence agree. If a request arrives on loopback but
|
||||
carries `Forwarded`, any `X-Forwarded-*`, or `X-Real-IP` header evidence, that
|
||||
forwarded-header evidence disqualifies the loopback locality claim, and the
|
||||
pairing path requires explicit approval instead of silently treating the
|
||||
request as a same-host connect. See
|
||||
[Trusted Proxy Auth](/gateway/trusted-proxy-auth) for the equivalent rule on
|
||||
operator auth.
|
||||
|
||||
## 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.
|
||||
- In remote mode, pairing happens against the remote Gateway's store.
|
||||
|
||||
## Related
|
||||
|
||||
- [Channel pairing](/channels/pairing)
|
||||
- [Nodes CLI](/cli/nodes)
|
||||
- [Devices CLI](/cli/devices)
|
||||
Reference in New Issue
Block a user