Commit graph

17 commits

Author SHA1 Message Date
Peter Steinberger
082eef88b6
fix(node): keep the selected profile's service name for node install --profile (#180288)
node install --profile took the default node's service name, so a profiled node collided with the default service. The node-service identity owner now preserves the selected profile across installation, lifecycle commands and the stored environment; the default profile is unchanged. Supersedes #178342, thanks @MertBasar0.

Co-authored-by: MertBasar0 <mertbasar0@hotmail.com>
2026-10-21 22:52:35 -07:00
Peter Steinberger
73ac5d64fd
chore: update stock Bun to 1.4.3 (#171627)
Select stock Bun 1.4.3 consistently across setup, cached archives, Docker, package smoke, supported-runtime documentation and runtime admission. Preserve the pinned OpenClaw fork and remove the upstream-fixed grapheme lookup workaround. Reject obsolete Bun in the standalone Team controller before activation.

Proof: focused Node/stock Bun/pinned-fork behavior, admission and preinstall checks; setup/action syntax checks; verified multi-architecture Docker digest; 26 passing Linux controller tests and a before/after activation regression. Independent Codex P2 review and exact-head ClawSweeper found no remaining actionable issues.

Exact-head CI: https://github.com/openclaw/openclaw/actions/runs/38617609269. The PR records inherited wrapper-inventory failures and Node-only load-wave timing pressure with baseline/source evidence; native admission retains the failed CI result and checks all remaining gates.
2026-10-14 12:35:35 -07:00
Ayaan Zaidi
2c87e33399
feat(sessions): run sessions in existing folders on paired devices (#169722)
Related: #136048 (non-Git directory semantics) and #142521 (non-Git worker workspace context; this PR does not implement ephemeral cloud workspaces).

## What Problem This Solves

Device-hosted sessions required a Git repository or managed worktree, a constraint inherited from ephemeral cloud sessions, even when a paired personal machine should simply work in an existing non-Git folder.

## User Impact

Users can choose **Existing folder on device** in New Session and run a session directly in an existing folder on a paired session-host device. OpenClaw does not create Git metadata, copy the folder to the Gateway, or sync, reconcile, reset, or delete it.

- `nodeHost.workerRuns.folders` is node-local consent: unset allows existing directories, a non-empty list restricts selection to those roots and their descendants, and `[]` disables the feature and its capability advertisement.
- The session stays pinned to that device: Auto/cloud dispatch, Move, and Continue on Gateway are refused. Attachment staging and the Gateway Files panel are unavailable for these sessions.
- Worker HOME stays in private node storage. Files are not locked against external changes.

## Why This Change Was Made

- `sessions.create` accepts `deviceFolder: { deviceId, path }` (requires `operator.write`); the binding is stored on the existing session entry and the `device-folder` workspace kind reuses the worker execution and lifecycle owners.
- The node owns folder policy and re-validates on every use: absolute or `~` paths only, realpath resolution (symlinks cannot escape a root), no filesystem root, no overlap with private worker storage, and a dev/ino fence between acquisition and execution. One shared rule in `node-workspace-protocol.ts` forbids preparing, resetting, transferring, seeding, or quiescing a device folder, for both ingress parsing and direct node execution.
- Gateway shutdown reads the teardown scope from the durable placement, so stopping an attachment with no in-memory tunnel no longer reaches closed worktree admission.
- A restored existing-folder create retry is owned by its frozen request; current device capacity or runtime no longer blocks it.
- Attachments are refused for device-folder sessions before anything is persisted: `chat.send` rejects them with a clear message, New Session blocks Send while attachments are attached, and worker media preparation never restages historical attachments into a device folder, so one refused file cannot make later turns fail.
- Security tradeoff: a session-host node already runs operator-directed agents with exec, so the allow-all default does not widen that boundary. The folder list is a selection guardrail, not an exec sandbox or a boundary against a compromised Gateway. Deployments that contain `operator.write` with `tools.fs.workspaceOnly` should also set a root list; the docs say so.
- Control UI selection/path input, plus docs for node configuration, session hosting, operator scopes, and workspace behavior.

## Screenshots

Synthetic mocked-Gateway captures (no real paths or tokens).

**Before — only the managed/new-workspace choice**

![Before: no existing-device-folder option](https://github.com/user-attachments/assets/b9697664-c8ed-414f-95e3-e34f987051d8)

**After — desktop existing-folder input**

![After: existing folder on device, desktop](https://github.com/user-attachments/assets/b721aeec-7e23-495a-b454-d85d97af8c79)

**After — mobile layout**

![After: existing folder on device, mobile](https://github.com/user-attachments/assets/2f2ff9fe-0c1f-4a85-9db7-9a1d9a5e534b)

## Evidence
- **Cross-machine E2E on the final feature code:** a disposable Gateway on one Mac (tailnet TLS, token auth, QA mock model) and a session-host node on a second Mac, both on the same exact head, with `folders` unset and an existing non-Git target folder on the second Mac. Pairing and capability advertisement; create + explicit dispatch (`active`, workspace = the device folder); hosted exec wrote `pwd` and `hostname` into the folder, confirmed by independent readback on that Mac; node restart and an unclean Gateway restart, then a second turn appended to the same file; `~` and a Gateway-only path rejected; reclaim + delete left the folder, sentinel, and absence of `.git` unchanged with no copy in Gateway state.
- **Isolated single-host E2E (25 checks, all pass):** missing/root/file/relative/private-storage rejections; auto-dispatch rejection; node restarts; clean Gateway shutdown with a live tunnel and with a durable attachment but no tunnel; Gateway restart and resume; symlink escape and outside-root rejection under a root list; `[]` disables the capability.
- **Attachment refusal, live:** on the isolated Gateway, a `chat.send` with a text attachment to a device-folder session returns the refusal, the attachment never enters history, and the next text turn and later turns after node and Gateway restarts complete in the folder.
- **Authority, live (real Gateway WebSocket, no mocks):** an operator client granted only `operator.read` + `operator.sessions.write` sends `sessions.create` with `deviceFolder` and gets `FORBIDDEN` / `missingScope: operator.write`; no session row exists afterwards and a debugger logpoint on the node records no workspace command for that request. The full `operator.write` client creates and dispatches the same folder (`active`).
- **Device revocation during awaited admission, live:** with the node process tree stopped (`SIGSTOP`) while `sessions.create` and `sessions.dispatch` wait on it, `devices remove` succeeds, then `SIGCONT`. Create fails with `Selected device changed during folder validation; retry` and leaves no row; dispatch fails with `Device worker installation connection is no longer current`; the node is then disconnected and its old token is rejected. Known bounded behavior: the read-only folder validation request already delivered to the stopped node still runs once on resume (1 ms after removal) before the close frame arrives; its result is discarded, nothing is persisted, and no session command runs. Unit coverage also revokes the caller (real `captureGatewayDeviceRevocation` capture) and the device during validation.
- **Requesting-operator revocation during awaited admission, live:** separate signed operator devices with `operator.write`, real paired node stopped with `SIGSTOP`. Removing the operator while `sessions.create` waits on folder validation: the client gets `gateway closed (4001): device removed`, `sessions.describe` returns `{"session":null}`, the key is absent from `sessions.list`, and the node trace has no worker execution command. Removing a different operator while `sessions.dispatch` waits: the session moves from `provisioning` to `{"state":"failed","terminalReason":"Gateway requester authority changed"}`, `hasActiveRun` is false, and no worker command runs. As with device revocation, one already-delivered read-only folder validation runs on resume and its result is discarded.
- **Inline initial turns are refused:** `sessions.create` with `deviceFolder` plus `message`, `task` or `attachments` is rejected before node I/O; the UI already sends the first message through `sessions.send` after dispatch. The three new cases fail with the validator removed (`3 failed | 12 passed`) and pass with it (`25 passed`).
- **Upgrade compatibility, live:** the `main` merge-base build created a plain session and an explicit-cwd session (turn `ok`); the PR build on the same state dir lists and describes both with their original IDs, a new turn on the old session completes `ok`, and Doctor exits 0 with no session-row load errors.
- **Upgrade from stable `v2026.10.3`, live:** the stable build created a plain session with a completed turn, an explicit-cwd session, a custom model alias and a non-default `thinkingDefault`. Direct startup on the new build exits 78 with "uses schema version 24; … run openclaw doctor --fix" on both this PR and the plain `main` merge base (inherited). After `openclaw doctor --fix --non-interactive` (`v24 -> v26`), the PR Gateway starts, both old sessions list and describe with their original IDs, both settings read back unchanged, a new turn on the old session returns `ok`, and a new device-folder session created after the upgrade dispatches and runs a turn in the folder. One session-event publication warning about an unsettled worker-environment mutation appeared during that dispatch; the turn still succeeded.
- **Focused tests:** device-folder node, tunnel, create, protocol, media, submit-gate and draft-state suites pass; the restored-retry and history-restage regressions fail before their fixes.
- **Hosted CI:** on the exact head `364a000`, every job passes on the first attempt, and so does the Security Review, except `checks-ui-e2e (8/9)`. That job runs `plugins-lifecycle.e2e.test.ts`, which also fails the same way on plain `main` (see the maintainer override comment). Earlier heads hit flaky qa-lab E2E tests and `main`-side ratchet failures that plain `main` also reproduces.

## Merge Danger

Additive optional protocol field and session-entry JSON field; no SQL schema change. Older Gateways reject the new `sessions.create` parameter; older nodes do not advertise the capability, so the option is not offered. Revert removes the feature; existing device-folder sessions would then be unrecognized but their folders are untouched.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-10-13 20:27:25 -07:00
Dallin Romney
a881a23b3b
docs: simplify tool, node, and plugin guides (#168699) 2026-10-10 22:20:07 -07:00
Ayaan Zaidi
84e59beb3c
fix(node): keep npx-installed services outside the npm cache (#168644)
## What Problem This Solves

Fixes node services installed through `npx openclaw connect <join-url> --service` or `npx openclaw node install` pointing at npm's disposable `_npx` cache.

## User Impact

The one-paste command installs the selected OpenClaw release into a durable npm prefix under the node's state directory before writing the service. Removing npm's exec cache no longer removes the service's executable. Installation prints the package version, chosen runtime, service command, and a fresh-npx node update command.

## Why This Change Was Made

The service argument resolver previously used the running package's resolved path. #167905 fixes pnpm's replaceable store-generation paths by selecting its existing stable link; npm exec has no stable link to select. This preserves that pnpm behavior and adds node-install promotion through the existing npm update owners (`resolveGlobalInstallTarget`, `runGlobalPackageUpdateSteps`, and their staged publication/rollback/cleanup).

There is one `<state-dir>/npm` prefix, not a prefix per release. The normal state-directory/profile resolver chooses its location. Run `npx -y openclaw@latest node install --force` on the node to update it, retaining the same profile/state-directory settings. An exact version or `@beta` can replace `@latest`. Reinstalling an already-selected version repairs damaged package files. No new settings or package manager.

This does not change `openclaw update` or its existing co-located Gateway selection behavior. Node auto-update still follows the node's npm channel, not the connected Gateway's version/build. The rebase preserves #168653's paired-credential filtering and explicit `--auth-from-env` service argument.

No overlap with Pash/Sarah changes.

## Evidence

Final code: `27b447dc07b737126124ff8847f2059cb8a4f921`. Packaged live proof ran on `292bd19fe934ecf4ee4e3b20b00a03a1b08f7933`; production feature code and docs are unchanged since that proof. The final revision only corrects regression-test platform handling and rebases onto green main.

- **Before:** the regression on baseline `e40606fdea` selected `.npm/_npx/.../node_modules/openclaw/dist/index.js`. In the live isolated replay, published `openclaw@2026.9.9` completed real join-URL enrollment; deleting its npm exec cache made the saved command exit 1 with `MODULE_NOT_FOUND`.
- **After:** the canonical self-contained package passed `scripts/package-openclaw-for-docker.mjs --skip-build --allow-unreleased-changelog` and its tarball inventory/import-graph integrity check. Real `npm exec --package=<candidate.tgz> -- openclaw connect <join-url> --service` promoted that selected package into the node's state-owned npm prefix. Both actual launchd and systemd generators selected the durable entrypoint. After removing `_npx`, executing the persisted command as a separate process connected to the isolated Gateway. The published driver's device identity and node settings were unchanged.
- **Upgrade and repair:** seeded the durable prefix with published `openclaw@2026.9.8`, retained the enrolled node state, and invoked the candidate through fresh npx with `--profile upgrade node install --force`. The prefix became the candidate version. Overwriting its `dist/index.js` and repeating the same command restored the exact original file bytes. The runtime, device identity, and node settings remained unchanged; the repaired persisted command connected after deleting `_npx` again. The prefix contained only `bin` and `lib`, with one `openclaw` package rather than per-release directories.
- **Package identity:** the unpublished candidate shares the version string `2026.9.9` with the public release. An isolated loopback npm registry served the canonical candidate tarball for that version, while forwarding dependency requests to npm. This prevents accidentally testing promotion of the older public package with the same version string; no product behavior was mocked.
- **Focused checks:** 50 tests passed across `src/commands/node-daemon-install-npx.test.ts`, `src/commands/node-daemon-install-helpers.test.ts`, and `src/cli/node-cli/daemon.test.ts`. The new regression uses actual generators and executes their commands after cache deletion. The final portability correction keeps systemd coverage on non-Windows platforms and reads launchd artifacts only on macOS. That corrected regression passed (1.285 s test execution), and the `core-test-commands` type graph passed. Its unchanged durable-path assertion failed on the baseline as expected (pueue 2903).
- **Changed checks:** formatting, conflict/line-count/assertion/SQLite/mock-export ratchets, config docs, plugin boundaries, dependency guards, and the core type graph boundary passed. The prior head's local core typecheck stopped on unrelated `channel-presence-policy.ts:519` TS6133; the branch is now rebased onto the main fix rather than carrying a separate repair.
- **Independent review:** LAND on exact final head `27b447dc07b737126124ff8847f2059cb8a4f921`, including the corrected test, updater-step adapter, and paired-auth preservation.

Sanitized live results:

```text
before-cache-deleted: exit 1 (MODULE_NOT_FOUND)
after:  /state/npm/lib/node_modules/openclaw/dist/index.js
node host gateway connected: ws://127.0.0.1:<isolated-port>
upgrade: 2026.9.8 -> candidate 2026.9.9
sameVersionRepair: true
runtimePreserved: true
pairingAndSettingsPreserved: true
connectedAfterCacheDeletion: true
```

**Native-supervisor limitation:** supervisor activation was not verified. Native service installation reaches the existing systemd user-session/profile guards in the unprivileged container; macOS refuses native installation from an isolated HOME. The proof runs actual generated service arguments as a separate live node, not under an activated OS supervisor. Launchd XML was decoded in Linux and passed through the production generated-environment resolver; the macOS regression exercises the actual plist reader. No HOME-guard bypass, privileged container, existing Gateway, or real laptop installation was used.

**CI:** [final-head run 38097819645](https://github.com/openclaw/openclaw/actions/runs/38097819645) passed, including production types, test types, lint, source contracts, and the corrected Linux regression. All applicable PR checks finished with no failure, cancellation, or pending check. The final head is rebased onto green main; no inherited failure waiver or SDK budget change is needed. ClawSweeper is **ready, 5/6**, and the independent reviewer returned **LAND**, both on `27b447dc07b737126124ff8847f2059cb8a4f921`.

## Bounded cost

Only npx-backed node installation needs an additional npm installation. It uses one state-owned prefix and the existing updater's staging, verification, rollback, and cleanup; it does not accumulate per-version prefixes. Reuse verifies the installed package and advertised content inventory, so a same-version damaged package is repaired. Ordinary node execution and model/session paths are unchanged. Non-npx installation adds only entrypoint inspection and installation diagnostics.

Source +104/-2, tests +88/-0, docs +34/-0; net +224 across seven files. The growth reuses the existing package updater rather than adding another installation or state owner.

## Merge Danger

Two-way code change, with a durable npm installation left in the selected node state directory if reverted. A revert restores the old npx service-install behavior; it does not remove an already installed package. Blast radius is headless node service installation and its printed diagnostics; Gateway installation and pnpm launcher resolution are unchanged.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-10-10 17:27:05 -07:00
Ayaan Zaidi
1bc9624c8a
fix(node): keep paired credentials after service enrollment (#168653)
## What Problem This Solves

Fixes paired nodes failing to authenticate after `openclaw connect <join-url> --service` when the machine already has credentials for another Gateway.

## User Impact

A node reconnecting to its saved Gateway uses its paired device token instead of an unrelated Gateway token or password from the machine's environment, configuration, or state-directory `.env`. Paired service installations no longer persist those ambient credentials.

Intentional shared-token/password connections remain supported through `openclaw node run --auth-from-env` and `openclaw node install --auth-from-env --force`. This exposes the existing foreground override and records that explicit choice for installed services. If it displaces an available paired credential, the warning names the selected environment variable, never its value. No new configuration keys or token migration are introduced.

## Why This Change Was Made

The node credential owner already recognized a saved pairing but still treated every populated `OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD` as an explicit override. The service planner also persisted those ambient values. Environment variables loaded from `env.vars` or the state-directory `.env` therefore had the same effect as an intentional override.

The runtime and service planner now share the same saved-endpoint eligibility check. Default reconnects leave device-token selection and refresh with `GatewayClient`; explicit shared auth uses the existing environment-only path. GatewayClient's trusted-endpoint retry policy is unchanged.

This retains the intent of [#31122](https://github.com/openclaw/openclaw/pull/31122) (intentional service environment credentials survive restarts), [#136253](https://github.com/openclaw/openclaw/pull/136253) (a co-located Gateway must not displace paired-node auth), and [#159360](https://github.com/openclaw/openclaw/pull/159360) (expired fallback setup codes may only resume the saved pairing; no expired bootstrap or shared-auth fallback).

## Evidence

- On baseline `e40606fdea`, an isolated Gateway issued a real join URL and a second isolated machine state completed bootstrap pairing through `connect --service`. The first handshake of the restarted real install-plan command failed authentication for process-env token, process-env password, config `env.vars`, and state-directory `.env`. All four sources were persisted into the service environment. CWD `.env` was already blocked by workspace policy; plain co-located `gateway.auth.*` was already correctly skipped.
- The repaired replay connected on the first handshake for all six sources, without persisting any ambient token/password. The explicit `--auth-from-env` shared-token control also connected, retained its selected credential in the generated service environment, and retained the explicit service argument.

  | Credential source | Baseline first handshake | Repaired first handshake | Repaired ambient credential persisted |
  | --- | --- | --- | --- |
  | Process token | Auth rejection | Connected | No |
  | Process password | Auth rejection | Connected | No |
  | Config `env.vars` token | Auth rejection | Connected | No |
  | State-directory `.env` token | Auth rejection | Connected | No |
  | CWD `.env` token | Connected (already blocked) | Connected | No |
  | Co-located `gateway.auth.*` | Connected (already skipped) | Connected | No |
  | Explicit shared-token flag | — | Connected | Yes, intentionally |

Sanitized authentication excerpts (ephemeral port replaced):

```text
baseline, ambient token:
node host gateway connect failed: unauthorized: gateway token mismatch (provide gateway auth token)

candidate, ambient token:
node host gateway connected: ws://127.0.0.1:<isolated-port>

candidate, explicit shared-token override:
node host: --auth-from-env selects OPENCLAW_GATEWAY_TOKEN instead of the paired device token
node host gateway connected: ws://127.0.0.1:<isolated-port>
```

- **Published-driver × candidate proof:** npm `openclaw@2026.9.9` performed real enrollment and generated service plans for both a join-enrolled node carrying unrelated ambient credentials and an intentional shared-token node without a cached device token. Candidate Doctor migrated their published schema 19 state to schema 20, then the candidate install planner/restart path connected both: the first using its paired token without retaining ambient auth; the second using the explicitly persisted shared token and `--auth-from-env`. Device identity, durable node settings, and the co-located Gateway settings were preserved in both cells.
- The no-cache shared-auth fixture used the published production auth-store owner to remove only its local node-token cache after the published connection; its identity and Gateway-side pairing remained intact. No raw database edits were used.
- **Revocation/final-effect proof on frozen head `dded84b33f34516638a4d2cad8b748be58521734`:** an explicitly approved `system.which` invocation returned `{"bins":{"true":"/usr/bin/true"}}`. After `devices revoke --role node`, restarting the same node with the valid shared Gateway token still present in its environment failed with `AUTH_DEVICE_TOKEN_MISMATCH`; the node never completed hello, and the real invocation returned `{"ok":false,"error":{"type":"cli_error","message":"node pairing changed while invocation was active"}}`. Device identity was unchanged. The live assertions completed; the initial report formatter rejected the CLI's trailing diagnostic footer, so the retained leading JSON objects were independently decoded and asserted.

```text
node host gateway connect failed: unauthorized: device token mismatch (rotate/reissue device token)
node host gateway reconnect paused after close (1008): unauthorized: device token mismatch (rotate/reissue device token) detail=AUTH_DEVICE_TOKEN_MISMATCH; waiting for operator action
```


- The runner regression failed on baseline for all three nonempty ambient credential combinations. The install-plan regression failed because ambient auth was persisted and the explicit service flag was absent.
- Six focused owner suites passed: runner, node runtime/install plan, install helper, node registration/lifecycle, and connect CLI. Existing GatewayClient and candidate-connection suites also passed, including trusted retry and refusal to retry untrusted token mismatches.
- Independent adversarial review returned **LAND** on final head `dded84b33f3`. `git range-diff` confirmed the two commits are patch-identical to the pre-rebase reviewed/proven version `8a906bd94e3`; the published-driver cells ran before that rebase, and the final authorization proof ran against a separately built, frozen checkout of `dded84b33f3`.

**Proof limitation:** the production canonical-home guard correctly refuses an OS-service installation under an isolated temporary HOME. The accepted isolation proof therefore executes the real `buildNodeInstallPlan` output (persisted command and environment) as the second process, against a live isolated Gateway. This is not a claim of exercising an actual OS supervisor. No existing Gateway, node service, or laptop installation was modified.
The published planner is private in the shipped bundle. Its isolation harness loaded a byte-identical copy of the shipped daemon chunk with **only** `export { buildNodeInstallPlan };` appended; authentication, service planning, and guards were not changed. Original chunk: 12,991 bytes, SHA-256 `7d67af30e747f36ea91d87f7b00fffb16ca7111e13dd1d4ce6195812b6de55da`. The actual published CLI performed enrollment. The temporary exported copy was removed afterward.

Final exact-head CI is **green**: [run 38089699521](https://github.com/openclaw/openclaw/actions/runs/38089699521). The previous inherited boundary failure was fixed on `main`, and this branch was rebased onto that fix. Local changed checks passed formatting, ratchets, core types, and core-test types, then stopped on pre-existing unrelated model-auth/solid-lint dead exports; the final hosted gates passed.


## Bounded cost

The runtime reuses the existing saved-endpoint/token eligibility read; only the explicit environment-override path additionally checks whether its warning displaces a pairing. Service installation adds bounded read-only checks of existing node state, identity, and token. No new tables, polling, retries, model calls, or background services are introduced. The new install-plan cases took 26 ms of test execution in the focused run; the six-suite invocation took 325.65 seconds including cold worker compilation.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-10-10 15:19:50 -07:00
Ayaan Zaidi
8f91e4bb65
improve: default device joins to session-host services (#168623)
## What Problem This Solves

Newly joined machines default to a foreground, command-only node, while New Session gives already-paired devices an invalid reconnect command and misleadingly says no session hosts are paired.

## User Impact

The CLI and Control UI now offer `npx -y openclaw connect <join-url> --service --session-host` by default. It installs a background node service that can run agent sessions, with a command-only service alternative. The UI shows the full command instead of clipping its consent flags.

Session hosting remains an explicit device-local choice in the pasted command; the node runtime's default is unchanged. Use hosting only on trusted shared Gateway infrastructure.

Already-paired devices get the supported `openclaw config set nodeHost.workerRuns.enabled true`, then `openclaw node install --force` instructions. The empty-host hint distinguishes pairing from enabling hosting.

## Why This Change Was Made

Preserves the one-paste, single-use onboarding contract from [#122499](https://github.com/openclaw/openclaw/pull/122499) and [#122635](https://github.com/openclaw/openclaw/pull/122635). Corrects the remediation introduced in [#145183](https://github.com/openclaw/openclaw/pull/145183) using the saved-pairing service path documented by [#159810](https://github.com/openclaw/openclaw/pull/159810).

The join handler is its own lazy CLI module rather than adding more responsibilities to the device-management runtime. `-y` only skips [npm's package-install confirmation](https://docs.npmjs.com/cli/v11/commands/npx/); it does not replace OpenClaw consent. No package version is pinned.

No overlap with Pash/Sarah changes.

## Evidence

- Regressions fail on baseline `7e23f938f8` for CLI text/JSON and paired-host/empty-host rendering.
- Real CLI before/after proof used separate isolated Gateways and state directories: the baseline printed `npx openclaw connect <url>`; the candidate printed the service/session-host command and command-only alternative. Both JSON invocations exited 0.
- CLI and lazy-loading tests: 52 passed. The Gateway setup and three UI rendering/picker suites pass after the scope reduction, as does the host-only browser regression. The five connection-dialog browser flows also pass.
- Independent exact-head review: **LAND**, no findings.
- [Exact-head CI](https://github.com/openclaw/openclaw/actions/runs/38083704554): **success**, 46 successful jobs, none failed or cancelled.
- [ClawSweeper](https://github.com/openclaw/openclaw/pull/168623#issuecomment-6101771154): **ready** on `d456a0ebad0fb6bd101821f0f8839896a7fde7f0`, no findings.
- Local `check:changed` passed its ratchets and core/test/UI typechecks, then stopped at the global unused-export scan. The same model-auth and Solid lint exports fail on clean `origin/main` (`e40606fdea96`); the additional pre-existing Solid test-helper export has already been removed upstream in [62629b0d6e](62629b0d6e). None of these files are changed here.

<details>
<summary>Observed CLI output from the isolated Gateway</summary>

The loopback port and single-use code below are normalized; both commands exited 0.

```text
$ openclaw devices join-code
http://127.0.0.1:<port>/j/<code>
npx -y openclaw connect http://127.0.0.1:<port>/j/<code> --service --session-host
Installs a background node service that can run agent sessions.
Command-only node: npx -y openclaw connect http://127.0.0.1:<port>/j/<code> --service

$ openclaw devices join-code --json
{
  "joinUrl": "http://127.0.0.1:<port>/j/<code>",
  "command": "npx -y openclaw connect http://127.0.0.1:<port>/j/<code> --service --session-host"
}
```

This was captured from the production-identical join handler at `ddebdaccc0a2f53d96ecb185daa54b4918233688`; the final revision removes the deferred picker change and updates its regression expectations without changing that handler.

</details>

### Join command

| Before | After |
| --- | --- |
| ![Before: foreground command-only enrollment](https://github.com/user-attachments/assets/0328ced1-06b0-4f2e-91ec-8bbb81935ee4) | ![After: service/session-host flags and command-only alternative](https://github.com/user-attachments/assets/bfa4f0b0-fb35-4938-9c60-37a178298ab0) |

### Already-paired device

| Before | After |
| --- | --- |
| ![Before: invalid targetless connect instruction](https://github.com/user-attachments/assets/ceab1193-553a-4b9a-a343-1ddd6898ddc1) | ![After: supported saved-pairing service instructions](https://github.com/user-attachments/assets/64eb71fb-4567-46f4-8092-bd311be56ce8) |

## Bounded cost

No additional RPCs, model calls, polling, retries, or background work. The existing join request and device inventory remain the owners. The command-only alternative uses the same single-use URL.

## Compatibility and Risk

No new config keys, protocol changes, worker-dialect changes, or runtime-selection changes. Reverting restores prior suggestions for future joins; devices already enrolled keep their explicitly enabled service and hosting consent until the operator changes them. The blast radius is device enrollment and New Session guidance, not session execution or authorization.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-10-10 14:08:06 -07:00
Peter Steinberger
9148fbb46a
fix(nodes): explain and recover session-host setup problems (#160188)
* fix(nodes): explain and recover session-host setup problems

Report unsafe workspace ancestry before dispatch, resume pending node pairing
after approval, explain runtime command availability at its authority owner,
and log inventory publication failures only when they change. Share inventory
validation and remove superseded policy and projection helpers.

* test(nodes): assert specific dispatch remediation messages

* test(nodes): align pairing retry coverage with node recovery

* fix(nodes): preserve desktop access after hosting failures

Keep connected-node environment availability separate from session hosting readiness and retain diagnostic placement refusal. Include Codex plugin installation in missing-command remediation. Cover the inventory-to-environment boundary with real connected-node enumeration.

* refactor(nodes): share runner declaration validation

Keep the status-wait capability added on main while extending the existing strict record schema with bounded hosting diagnostics. Remove the superseded declaration parser and clone closed worker-host snapshots through one path. Production code remains net zero against the refreshed base.

* fix(nodes): retain exhaustive command diagnostic states

* refactor(nodes): preserve explicit diagnostic return flow
2026-09-28 19:13:09 -07:00
Peter Steinberger
968a08c6d0
perf(nodes): reuse warm workers so node session turns start as fast as local ones (#160265)
* perf(nodes): reuse warm workers so node turns start as fast as local ones

Retain settled workers for two minutes with at most two idle children per
node. Negotiate node-worker-idle-retention-v1 across Gateway, node, and
bundle; fresh turn admission and credentials remain mandatory. Protect
background work and preserve durable process cleanup and reconciliation.

Expose reclaimable idle capacity to placement admission and the session
picker. Consolidate lifecycle helpers under the supervisor and use the
shared capacity parser. Document the bounded process-count and memory
tradeoff; include lifecycle, negotiation, and picker regression coverage.

fix(nodes): retry failed idle worker cleanup

Rearm the supervisor idle timer after a transient physical cleanup failure while retaining the durable slot and retirement fence. Cover automatic expiry and idle-limit eviction alongside explicit cleanup with a fake clock.

Fold the cancellation forwarding wrapper into its owner and remove the immediately repeated admission abort check. Preserve the existing authority, settlement, and shutdown guards.

fix(nodes): synchronize idle capacity protocol models

Regenerate the Swift worker-slot model for the optional reclaimableIdle field. The schema and named capability contract remain unchanged.

Inline the single-caller prepared workspace custody wrapper in the supervisor and move its four cases to the public launch entry point. Preserve exact acquisition, abort, shutdown, and release ordering while keeping the complete branch production delta negative.

chore(nodes): prune stale worker assertion allowance

Remove the exact one-entry assertion baseline for worker-command.runtime.ts after the unsafe assertion was eliminated. Keep the scanner and all unrelated allowances unchanged.

fix(nodes): keep supervisor dependencies on protocol owners

Import protocol parsers and types directly from their canonical leaf, including the QA fixture, and remove the redundant supervisor-control re-exports. This breaks the dependency cycle without duplicating the wire contract.

Keep the runner cancellation mock and shutdown callback asynchronous with the real Promise<void> contract, including teardown restoration.

fix(nodes): complete asynchronous cleanup contracts

Join cancellation cleanup in callers and tests while preserving parallel shutdown settlement and expected failure identity. Exclude absent cleanup owners before aggregation and keep stale-connection rejection unchanged.

Use immutable worker input, type-only journal imports, stable sorted idle projections, and receiver-bound fault injection. Remove redundant descriptor bookkeeping and finish the test lint cleanup without changing limits or suppressions.

* test(nodes): disambiguate inventory fixtures
2026-09-28 16:07:44 -07:00
Peter Steinberger
1b5ac171f6
fix(pairing): device join codes ignore gateway.publicOrigin behind public ingress (#159465)
* fix(pairing): honor gateway.publicOrigin for device join codes

A loopback Gateway behind public HTTPS ingress sets gateway.publicOrigin, and
cloud node enrollment already used it, but device join codes, the device-pair
plugin, and Doctor's node-hosting check each resolved the pairing endpoint on
their own and ignored it, failing with advice that omitted publicOrigin.
Make src/pairing/setup-code.ts the single owner (device-pair publicUrl
override, then gateway.publicOrigin, then existing discovery), expose it to
the device-pair plugin through plugin-sdk/device-bootstrap, remove the
plugin's duplicate resolver and enrollment's private fallback, and share one
loopback error that names gateway.publicOrigin.

* fix(pairing): preserve explicit remote endpoint selection

Honor preferRemoteUrl before gateway.publicOrigin so qr --remote keeps
its selected endpoint aligned with remote credentials. Retain publicOrigin
as this Gateway's ingress ahead of automatic Tailscale, remote, and bind
discovery, with the pairing-specific override taking precedence.

Cover configurations with both URLs at the resolver and QR CLI boundaries,
and correct the CLI, cloud-worker, and SDK precedence documentation.

Validation: 221 focused tests passed; scoped core, extension, script, and
test typechecks passed; independent review found no actionable P0-P2 issues.

* fix(pairing): preserve device routes with explicit cloud ingress preference

Keep existing device join-code, QR, and /pair discovery order. Use
publicOrigin only at the loopback fallback, while cloud enrollment asks the
same resolver to prefer public ingress at both preparation and issuance.

Use the canonical lazy runtime binder for the SDK export so the collision
guard recognizes one implementation without loading setup code at startup.
Document the two call-site intents and cover Tailscale, LAN, loopback, and
cloud enrollment behavior.

Validation: export-name collision and SDK surface guards passed; 222
focused tests and scoped typechecks passed; independent review was clean.

* chore(device-pair): drop the max-lines suppression the smaller plugin no longer needs

* fix(device-pair): preserve origin-only setup URLs

Retain the plugin command URL mapping through the shared pairing resolver while preserving context paths for join codes and cloud enrollment. Clarify public-origin fallback and remote QR prerequisites.
2026-09-27 12:07:50 -07:00
Peter Steinberger
2a1402cd3e
fix(startup): keep legacy state repair in Doctor (#151025)
* fix(startup): keep legacy state repair in doctor

Separate current-state startup readiness from explicit Doctor migrations.
Preserve current config recovery, quarantine, device identity checks,
lease fencing and updater completion ownership.

Remove automatic startup migration/checkpoint and node-host import paths.
Keep shipped read-only context and roster projections unchanged.

* test(doctor): align proof callers with repair ownership

Keep survivor fixture setup in caller order and select the shared Doctor
flow separately from the channel-specific proof. Include the complete
isolated diagnostics dependency closure.

Prove ordinary startup preserves legacy directories before explicit
Doctor repair, and await SQLite worker closure before test cleanup.

* test(startup): verify index repair through gateway maintenance

* test(doctor): align cutover fixtures with state owners

Reacquire the database after Doctor retires its generation, retain real session history for startup refusal, and keep pending reads at their actual owners. Distinguish the reused readonly reader from independent snapshot-token children and verify every child settles.

* fix(doctor): preserve image activation and published migration receipts

Run the shared noninteractive Doctor owner before default and Compose Gateway
activation so retained Docker volumes keep their published upgrade path while
ordinary Gateway startup stays readiness-only. Preserve root selectors and
settle interrupted repair before executing the original command.

Retain the path-wide tombstones emitted by the published restart-sentinel
importer, including consumed notices, and validate completed source decisions
before retiring recreated inputs. Add the root-image activation lane and causal
receipt coverage without introducing a schema, option, or second importer.

* test(docker): preserve release state pairs in upgrade proof

Use the existing synthetic v2026.9.2 corpus instead of combining a July shared database with a later agent schema. Preserve its deletion journal, registry, both transcript payloads, paired backups, and unsafe-state controls. Register the two shell-launched helpers with Knip.

* test(docker): normalize persisted schema snapshot rows

Compare node:sqlite schema rows using the same plain-object representation as the saved JSON preimage. Keep strict schema, row, and value-type assertions intact.

* fix(models): retain discovered models after refresh failures

Record successful legacy catalog results at the producer boundary so unavailable refreshes retain the accepted inventory. Preserve explicit outcomes, advisory SDK fallback behavior, and first-discovery starter policy.

* fix(models): preserve skipped catalog outcome semantics

Mark bundled static, configured, and advisory catalog projections with
explicit empty outcomes so legacy success inference cannot promote them
to observed account inventory. Preserve live outcomes and helper types.

Keep exact auth provenance histories and move existing fixture/policy
code into focused owners where required by the line-cap ratchet.

Validation: 447 producer and sibling cases, 56 shared self-hosted cases,
95 auth/policy cases, causal missing-outcome failures, maintained checks,
and independent review.

* test(plugin-sdk): keep discovery loader types acyclic

Move the shared loader type into a leaf consumed by both discovery
contract helpers. Preserve its public provider-test-contracts export
without a child-to-parent type import cycle.

Validation: maintained Madge check reports zero cycles; core, all core
test graphs, extension test types, lint, formatting and independent
review pass. Runtime behavior and previous catalog proof are unchanged.

* fix(plugin-sdk): mark generated static catalogs explicitly

Keep the generated non-live, non-strict catalog adapter from claiming
successful acquisition for manifest or configured rows. Preserve null,
errors, strict and custom callbacks, static catalogs, and public types.

Validation: three existing controls fail before the correction; all49
owner and sibling cases pass afterward, with types, lint, line caps and
fresh independent review clean.

* test(gateway): cover restart import during state retirement

Exercise canonical database close while a legacy notice read is paused. Verify admission rejection, retained canonical and source bytes, no migration receipt, and joined maintenance custody without changing the accepted sidecar-stop drain contract.

* fix(test): drain sharing fixtures before removing state

* test: bind retirement regression to its own worker

* docs(docker): clarify automatic Doctor activation

* test(doctor): keep readiness fixture runtime private

* test: stabilize shared skill watcher fixture roots

(cherry picked from commit 15606be10ed61be99931fcf3bb300ebdb475ad89)

* perf(tooling): share indexed scope parsing for artifact scans

(cherry picked from commit 6e6eef9f5b556e0005b16363169e6a85cd310ad0)

* fix(team-reports): use source owners in scheduler tests

The source barrel retired in #157819, but the scheduler tests still imported
it, breaking the extension test typecheck on main. Import the Discord and
GitHub owners directly, matching the production caller.

Validated the original TS2307/TS7006 failure, the corrected extension type
graph, all 36 scheduler tests, changed checks, and independent P2 review.

(cherry picked from commit 7c4866c73b)

* test(install): isolate global npm configuration in version fixtures

Use a controlled absent global config inside the fixture home so the
release helper does not query the deliberately narrow npm stub. Keep
predecessor selection, fresh-install behavior, and expected exits intact.

(cherry picked from commit 0a9eefc76c82d81bd134add3ac3d269ae1b607bd)

* test(docker): verify the complete image activation entrypoint
2026-09-25 13:43:40 -07:00
Peter Steinberger
341f73b48b
fix(node): wait for resource cleanup before shutdown completes (#156995)
* fix(node): wait for resource cleanup before shutdown completes

Join asynchronous provider disposal, disconnect cleanup, and client drainage.
Fence late callbacks and starts, retain terminal failures, and retry only
owners that keep unfinished retirement work.

Preserve synchronous watcher callbacks and the existing public SDK exports.

* fix(plugins): drain desktop cleanup and remove contract import cycle

Wait for asynchronous provider watchers before the private computer worker
exits, and propagate cleanup failures through its terminal completion.

Route the existing SDK registration exports directly to their owner so
the shared wire schemas no longer import the registration runtime.
2026-09-24 20:46:13 +00:00
Peter Steinberger
3c4c111b0f
feat: automatically update idle headless nodes (#151545)
* feat(node): update headless runtimes automatically when idle

Stage private packaged node runtimes and activate only after all owned work
and cleanup settle. Preserve pairing and launch options, keep automatic
activations at least 12 hours apart, and leave shared-state migration and
Gateway installation ownership with the normal updater.

Document defaults, opt-outs, runtime version discovery, and recovery.
Support recoverable Windows selector replacement across service restarts.

Refs #151462

* fix(node): complete auto-update integration and settings defaults

Capture the launcher in compiled test runtimes and trusted wrapper inventories, register the package update proof, and use the canonical Kysely read path. Preserve the inherited enabled state in the config UI and align caller tests with the node shutdown lifecycle.

* fix(node): preserve retained plugin work during automatic updates

Require an explicit idle result from plugin commands, preserve older-plugin work, and connect bundled lifetime owners to automatic node updates. Join canceled inference and failed terminal cleanup before command settlement; keep historical state repair with Doctor. Document compatibility and extend installed-package proof for retained legacy plugin work.

* test(browser): align idle-work fixtures with runtime exports

* test(browser): extract proxy request fixtures

* test(node): retain idle assertions across native cleanup
2026-09-18 06:20:27 -07:00
Peter Steinberger
5278a8f2ed
feat: share selected sessions read-only with a paired team Gateway (#136253)
* feat(node-host): advertise an explicit node command allowlist

Persist exact node command selection and restrict ancillary publication and hosting. Preserve the unchanged assertion baseline under the work-order stop rule; check:changed requests removing the obsolete runtime.ts count (2 to 0).

* feat(plugin-sdk): session transcript catalog reader

Expose bounded read-only native display pages and portable attribution through the existing runtime subpath. Keep pagination scoped to the original active transcript branch and allow an explicit bounded native cursor length.

* feat(session-share): read-only OpenClaw session catalog across paired gateways

Publish explicitly selected native session groups through two paired node commands. Validate the closed wire contract, reject remote profile claims, and keep receiver identity binding opt-in and display-only.

* fix(gateway): show published session catalogs to view-scoped roles

Let publication consent satisfy catalog read visibility for roles allowed to view others, while owner-only and unprofiled callers stay hidden. Preserve published attribution without accepting a remote local-session adoption claim. Regression tests reproduce four pre-fix failures; final validation stopped at the work-order baseline gate.

* docs: session sharing across gateways

Document sessions-only node setup, explicit publication groups, receiver attribution, view-scoped catalog access, and read-only limits. Add the bundled plugin inventory and generated reference entry. Live proof runbook remains outside the repository; build and rig execution are blocked by the work-order baseline restriction.

* fix(session-share): preserve source storage and paired reconnects

Respect configured stores through listing, paging, and revocation. Keep cold listings available and bound raw transcript reads. Prefer the established paired node credential on service restart, suppress unrelated host metrics, and refresh the approved plugin configuration docs.

* refactor(gateway): separate authorized catalog reads

Keep the catalog dispatcher within its owned scope and preserve post-read role checks and sender projection. Align the rebased tests with their shared setup and imports.
2026-09-12 13:35:17 -07:00
Vincent Koc
0d66d24fb1
docs: fix verified accuracy findings in channels, cli, nodes, and reference (#143950)
* docs: fix verified accuracy findings in channels, cli, nodes, and reference

Close the open `accuracy` ledger rows for docs/channels/, docs/cli/,
docs/nodes/ and docs/reference/ that survived verification against source.

Each edit is backed by code or by a dated commit/release:

- Name the release for previously unscoped time-relative claims
  (Slack `progress` default 2026.8.1, Telegram preview default 2026.8.1,
  Discord DAVE receive recovery 2026.2.24, gateway ownership contract
  2026.8.1, cron `--deliver`/`cron.failureDestination`/file store,
  nodes `nodes.run` removal 2026.3.31, media `{{Attachment*}}` rename,
  presence clear action, memory-config embedding identity, schema-19
  `consumed_event_id`, `[view ...]` -> `[embed ...]`).
- Delete time-relative wording where no release record exists and state
  present behaviour instead (mattermost, msteams, groups, audio, camera,
  computer-use, secretref store scope, full-release-validation alias).
- Correct concrete defects: matrix `threadReplies` default, audio
  `gpt-transcribe` -> `gpt-4o-transcribe`, camera `--duration-ms`
  (no such flag), signal quickstart probe command, googlechat macOS
  bind-address check, discord `bash` fences holding slash commands,
  usage-window provider lists, RELEASING script naming, zalo disclaimer
  placement, talk.md placeholders, images.md duplicated vision rule.

`pnpm check:docs` passes. `docs-link-audit --anchors` reports the same 3
pre-existing `#windows-app-node` errors as the merge base.

* docs(nodes): restore the approved media-template deprecation window

ClawSweeper is right: `src/plugins/compat/media-legacy-projection.ts` marks
`{{MediaPath}}`, `{{MediaUrl}}`, `{{MediaType}}` and `{{MediaDir}}` deprecated
with `removeAfter: "2026-10-01"`, gated on a clean published-plugin artifact
sweep. "No removal is scheduled" was wrong.

Both pages now keep the deprecated status, state the approved date and its
gate, and link the record's own docs target
(/plugins/sdk-migration/compatibility-policy#media-legacy-projection).
2026-09-10 18:58:15 +08:00
Yuval Dinodia
69758a9ff6
docs(nodes): complete the two-stage approval flow (#136268)
Related: #128446, #98045 (both remain closed)

## What Problem This Solves

Several manual node setup paths stopped after device approval. A node paused for pairing could remain disconnected; after reconnecting, its initial command surface could still be unapproved. Troubleshooting omitted that separate gate, while bootstrap instructions incorrectly said enrollment never approved the first declared surface.

## Why This Change Was Made

The current CLI, node-host, pairing/status, Windows, and troubleshooting pages now lead through device approval, restarting or rerunning a paused node, inspecting the separate surface request, and approving its distinct request ID. Initial empty surfaces are distinguished from pending expansions that retain approved, still-declared commands. SSH-verified and administrator bootstrap enrollment are distinguished from trusted-network device-only approval.

The join-code reference shows the exact `npx openclaw connect <url>` output and current core ownership. It distinguishes reachable TLS endpoints, explicitly configured loopback URLs, default-loopback URL-discovery refusal, and direct plaintext-LAN setup codes. The invoke reference retains the existing 30,000 ms transport default and explains its invoke-plus-10,000 grace. The current configuration example uses `tools.exec.mode`.

The old nodes index remains a routing page with its stable anchors. Already-landed identity, revocation and re-pair guidance is preserved in the current owning pages.

## User Impact

Operators can complete manual enrollment and identify the correct missing approval before changing shell policy. Bootstrap consent, later expansion approval, Gateway command policy, node-local exec approvals and platform permissions remain distinct. This changes documentation only; runtime, configuration schema, storage, protocol and security policy are unchanged.

## Evidence

- Complete source-to-document audit against pinned main `6d8281b385`, including reconnect, surface reconciliation, bootstrap/trusted-network controls, core join routes, timeout registration/resolver and focused tests. Windows reconnect wording was checked against the separately maintained client source.
- Fresh source reviews resolved the three initial documentation findings; the final revision has no actionable source-to-document finding.
- Native docs inventory, changed-file checks, scoped MDX, Markdown lint, configuration examples, glossary, formatting and whitespace checks passed. The real publishing-parser anchor audit validates the changed documents. Its full run retains three unchanged maturity-page fragment errors also reproduced on pinned main with the same actual ClawHub documentation source.
- Full before/after Markdown and shared-parser HTML artifacts are retained. No node, Gateway, pairing, token, platform-permission or provider command was executed; tests were read as source evidence rather than reported as fresh runtime passes.

Thanks @yetval for the original cross-page correction and follow-up fixes. The contributor commits and previous review history are preserved; the older plugin prerequisite and 10-second invoke-command default are superseded by the current source facts above.

Independent artifact acceptance checked every affected manual entry path, including the standalone headless recipe and Windows troubleshooting. Its initial timeout-condition finding was corrected and freshly reviewed; final artifact results are 15 pass, 0 fail and 3 source/delivery limitations. Separate source evidence covers those source-only facts. The signed candidate passed the normal native commit hook with matching complete local/native indexed trees.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-09-09 11:27:24 +05:30
Vincent Koc
e427f280d9
docs(nodes): split the nodes overview by reader job (#142779)
* docs(nodes): split the nodes overview by reader job

docs/nodes/index.md was 68,029 bytes, 8,871 words and 35 headings mixing
pairing how-tos, node-host setup, session hosting, command-policy reference and
per-platform allowlists in one page. One H2, "Remote node host (system.run)",
parented 17 H3 sections that were not about system.run, and the page ended with
no next-steps list.

The page already lived in a directory with eleven siblings (audio, camera,
computer-use, images, location-command, media-playback, media-understanding,
presence, talk, troubleshooting, voicewake), so this extends that directory
rather than creating a parallel one. index.md becomes a real index at the same
/nodes route: intro, a "Node pages" list that also names the eleven existing
siblings, and the anchor table below.

Children, one per reader job:

- pairing-and-status.md - approve a node, read status and host stats, upgrade
  a fleet across the N-1 protocol window.
- node-host.md - foreground, service, SSH-tunnel and headless node hosts,
  gateway preconditions, identity state, and the system.* command surface.
- node-exec.md - allowlist commands, point exec at a node, raw node.invoke,
  and exec node binding.
- mcp-and-skills.md - node-hosted MCP servers, node-hosted skills, and local
  Ollama inference.
- session-hosting.md - nodeHost.workerRuns, device placement and capacity,
  and container isolation.
- session-catalogs.md - Codex, Claude, OpenCode and Pi session discovery and
  continuation on paired nodes.
- file-transfers.md - terminal uploads and the File Transfer plugin tools.
- command-policy.md - the platform default allowlists, dangerous-command
  opt-ins, gateway.nodes/tools.exec config, and the permissions map.
- device-commands.md - widget panel, camera, screen recording, location, SMS
  and device data CLI helpers.

Anchor strategy

Per-anchor redirects are not possible: redirectSource() in
scripts/lib/docs-redirects.mjs rejects any source containing [?#]. Every anchor
the old page published is therefore kept alive on the index itself as authored
<a id="..." /> stubs inside a "Where each section moved" list, each pointing at
its new home. Ids were computed with parseDocsDocument, not a slug
approximation, so the thirteen punctuated headings keep both their encoded and
their cleaned id (for example pairing-%2B-status and pairing-+-status). All 48
ids the pre-split page published resolve on the new index; the index publishes
only two ids of its own, node-pages and where-each-section-moved, so no stub
collides with a heading the index still owns. parseDocsDocument reports zero
collisions on the index and on every child.

Losslessness

Reassembling the 35 section bodies reproduces the original body byte for byte,
apart from the two declared link retargets below. Counts, original body vs
children:

- words 8,634 -> 8,634
- characters 66,320 -> 66,358 (+38, the two retargets)
- code fences 29 -> 29, identical fence for fence as a multiset
- markdown links 30 -> 30
- table rows 16 -> 16, all three tables byte-identical
- the per-platform default-allowlist table is byte-identical: 8 rows, with
  iOS 11, watchOS 3, Android 19, macOS 14, Windows 6 and Linux 2 commands

No prose was rewritten. Two intra-page fragment links whose target moved to a
different child, both `](#command-policy)` in device-commands.md, became links
to /nodes/command-policy#command-policy; that is the whole +38 characters.
Sections keep their original relative order within each child, so the five
directional cross-references the page carried ("the environment fallback
above", "see above", "see below", "the static platform-default table above")
all still resolve on their own page.

Also updated: the "Nodes and media" nav group in docs/docs.json, eight in-repo
deep links repointed at the new pages (docs/releases/2026.9.2.md left
untouched, its anchor still resolves through the stubs), fourteen zh-CN
glossary entries for the new titles and index link labels, and
src/docs/config-path-docs.test.ts, which asserts on the `openclaw config`
bracket-path examples that now live in node-exec.md.

Closes audit findings: r3-0443, r3-0445, r3-0447

* docs(nodes): link the relocated device command examples from the exec page

The exec page's "(camera, screen, location, below)" pointed at helpers the
split moved to /nodes/device-commands. Replace the directional word with a
link. Found by ClawSweeper; the orphan-reference scanner's patterns do not
match a bare trailing "below" with no noun phrase in front of it.
2026-09-09 10:44:24 +08:00