Skip to content

[Bug]: file-transfer plugin nodeHostCommands not advertised by Windows node host on live handshake (2026.5.3-1) #77730

@produtoramaxvision

Description

@produtoramaxvision

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

After upgrading from 2026.4.262026.5.3-1, the bundled file-transfer plugin's node-side commands (dir.list, file.fetch, dir.fetch, file.write) are never advertised by the Windows node host during the live WebSocket handshake, even though the plugin manifest has enabledByDefault: true, the plugin is enabled in both gateway and node-local openclaw.json (plugins.entries.file-transfer.enabled = true + plugins.allow includes file-transfer), and the gateway side correctly registers the plugin (12 plugins listening, file_fetch/dir_list tools surfaced to agents). All gateway invocations of these tools fail with node command not allowed: the node (platform: win32) does not support "dir.list", which means nodeSession.commands (live handshake payload) does not include the plugin's nodeHostCommands entries.

Steps to reproduce

  1. Pair a Windows 11 node to a Linux ARM64 (Ubuntu 24.04) gateway running openclaw@2026.5.3-1 via TLS over Tailscale: openclaw node install --host <gateway>.tailnet --port 8443 --tls --tls-fingerprint <fp> --display-name <name>.
  2. On the gateway, ensure config has:
    "plugins": {
      "allow": ["file-transfer", ...],
      "entries": { "file-transfer": { "enabled": true, "config": { "nodes": { "<name>": { "ask": "off", "allowReadPaths": ["C:\\Users\\<u>\\Desktop"], "allowWritePaths": ["C:\\Users\\<u>\\Desktop"], "denyPaths": [...], "maxBytes": 16777216, "followSymlinks": false } } } } }
    },
    "gateway": { "nodes": { "allowCommands": ["file.fetch", "file.write", "dir.list", "dir.fetch"] } }
  3. On the Windows node-local ~/.openclaw/openclaw.json, ensure:
    "plugins": {
      "allow": ["browser", "device-pair", "file-transfer", "openai"],
      "entries": { "file-transfer": { "enabled": true } }
    }
  4. Restart gateway service AND node host service (Scheduled Task or openclaw node restart).
  5. Confirm openclaw plugins list on Windows shows File Transfer ... enabled, and the plugin physical path <npm-prefix>/node_modules/openclaw/dist/extensions/file-transfer/ exists.
  6. From any agent chat with the gateway, request the agent to call dir_list against the paired Windows node.

Observed gateway log line:

[ws] ⇄ res ✗ node.invoke 1ms errorCode=INVALID_REQUEST errorMessage=node command not allowed: the node (platform: win32) does not support "dir.list" conn=... id=...

Expected behavior

Per extensions/file-transfer/index.ts, the plugin exports nodeHostCommands: [{command: "file.fetch", ...}, {command: "dir.list", ...}, {command: "dir.fetch", ...}, {command: "file.write", ...}]. Per src/node-host/runner.ts, the node host should call:

const cfg = getRuntimeConfig();
await ensureNodeHostPluginRegistry({ config: cfg, env: process.env });
const pluginNodeHost = listRegisteredNodeHostCapsAndCommands();
// ...
commands: [...NODE_SYSTEM_RUN_COMMANDS, ...NODE_EXEC_APPROVALS_COMMANDS, ...pluginNodeHost.commands]

With file-transfer enabled in node-local config and enabledByDefault: true in the manifest, pluginNodeHost.commands should include ["dir.list", "file.fetch", "dir.fetch", "file.write"], and the connect-handshake commands payload should include them. The gateway gate at src/gateway/server-methods/nodes.ts would then pass:

const allowed = isNodeCommandAllowed({
  command,
  declaredCommands: nodeSession.commands, // includes dir.list ✅
  allowlist,                                // includes dir.list ✅ (via gateway.nodes.allowCommands)
});

Actual behavior

nodeSession.commands includes only ["browser.proxy", "system.run", "system.run.prepare", "system.which"] (the NODE_SYSTEM_RUN_COMMANDS set plus the bundled browser.proxy). It never includes dir.list, file.fetch, dir.fetch, file.write, even though:

  • plugins.allow and plugins.entries.file-transfer.enabled: true are set in the node-local openclaw.json.
  • openclaw plugins list on Windows reports File Transfer ... enabled (loaded successfully on the local CLI registry).
  • paired.json on the gateway has been refreshed (via openclaw node install --force) and contains the expected caps: [browser, file-transfer, system] + commands: [browser.proxy, dir.fetch, dir.list, file.fetch, file.write, system.run, system.run.prepare, system.which].
  • Gateway-side gateway.nodes.allowCommands correctly opts the dangerous commands into the allowlist (without this, the error is is not in the allowlist for platform "win32"; with it, the error becomes the node (platform: win32) does not support, confirming gates are independent).

The error stays the same across multiple node restarts (Stop/Start ScheduledTask, openclaw node install --force, manual node ... node run ... invocation, fresh node.cmd regeneration). The node host process is alive (PID confirmed), connected (paired.json lastConnectedAtMs updates on each reconnect), and apparently functional for system.run calls — only the plugin-derived commands are missing from the live handshake.

OpenClaw version

2026.5.3-1 (2eae30e) on both gateway (Linux ARM64) and node (Windows 11)

Operating system

Gateway: Ubuntu 24.04 LTS, kernel 6.17.0-1010-oracle, aarch64
Node: Windows 11 Pro 23H2 (10.0.22631.6199), x64

Install method

npm global on both sides (npm install -g openclaw@2026.5.3-1)

Model

google/gemini-3.1-pro-preview-customtools (via openrouter; tool call routing path is the standard agent loop, model choice is incidental — the failure is at the gateway↔node WS layer, not the model layer)

Provider / routing chain

Agent (gateway) → openrouter → google → tool-call → gateway server-methods/nodes.ts → gate fails before node.invoke is forwarded

Additional provider/model setup details

The failing tool is gateway-resident (file-transfer plugin's dir_list tool) which calls node.invoke with command: "dir.list" at the WS layer. Provider routing for the agent is healthy (exec, web_search_v2, etc. work fine in the same session). The failure path is purely:

agent → tool dispatcher → gateway nodes.ts isNodeCommandAllowed → reject (declaredCommands missing)

Additional context (full troubleshooting timeline)

I spent ~50 minutes reading source, applying every documented fix, and validating each gate independently. Summary of what works and what does not:

  • gateway.nodes.allowCommands gate (Gate 1) — passes after adding file.fetch, file.write, dir.list, dir.fetch to gateway config (dangerous: true commands per node-command-policy.ts require explicit opt-in; PLATFORM_DEFAULTS for windows does not include them).
  • paired.json cache (storage) — manually edited and confirmed refreshed after openclaw node install --force (caps, commands, version all current).
  • declaredCommands gate (Gate 2) — node host process never advertises plugin commands on the live handshake.

Per src/plugins/runtime/runtime-registry-loader.ts the loader is invoked with scope: "all" and config: <node-local-cfg>. The node-local config has plugins.allow: ["browser","device-pair","file-transfer","openai"] and plugins.entries.file-transfer.enabled: true. Same plugin manifest works on the gateway side (file-transfer is one of the 12 plugins listening; wiki_search etc. are also registered on the gateway).

Hypotheses I could not validate without source-side instrumentation:

  1. ensureNodeHostPluginRegistry may apply a scope filter that drops plugins whose register(api) requires a gateway-only API surface, even though their nodeHostCommands array could be registered in isolation.
  2. The plugin loader may require OPENCLAW_BUNDLED_PLUGINS_DIR env var to be set in node host context — node.cmd generated by openclaw node install --force does not set it.
  3. pluginNodeHost = listRegisteredNodeHostCapsAndCommands() may be returning {caps: [], commands: []} because the registry is loaded but not committed before the connect payload is built (race / lazy import).

Workaround: use system.run (which is in the platform default and gets advertised) to invoke powershell -Command "Get-ChildItem ...". Loses path policy / byte-cap / approval flow that file-transfer provides.

Happy to instrument and re-test if a maintainer can suggest where to add a console.error('[node-host] plugin nodeHostCommands:', pluginNodeHost.commands) print to confirm which hypothesis is real.

Sanitized config snippets reproducing the issue

Gateway-side (/root/.openclaw/openclaw.json):

{
  "plugins": {
    "allow": ["acpx","active-memory","browser","copilot-proxy","device-pair","diffs","discord","file-transfer","firecrawl","github-copilot","google","llm-task","lobster","memory-core","memory-wiki","open-prose","openai","opencode","opencode-go","openrouter","pinchtab","searxng-search","tavily","telegram","thread-ownership","voice-call"],
    "entries": {
      "file-transfer": {
        "enabled": true,
        "config": {
          "nodes": {
            "<node-display-name>": {
              "ask": "off",
              "allowReadPaths": ["C:\\Users\\<user>\\Desktop"],
              "allowWritePaths": ["C:\\Users\\<user>\\Desktop"],
              "denyPaths": ["C:\\Users\\<user>\\.openclaw","C:\\Users\\<user>\\.ssh","C:\\Users\\<user>\\.aws","C:\\Users\\<user>\\AppData"],
              "maxBytes": 16777216,
              "followSymlinks": false
            }
          }
        }
      }
    }
  },
  "gateway": {
    "nodes": {
      "allowCommands": ["dir.fetch","dir.list","file.fetch","file.write"]
    }
  }
}

Windows-local (C:\Users\<user>\.openclaw\openclaw.json):

{
  "plugins": {
    "allow": ["browser","device-pair","file-transfer","openai"],
    "entries": {
      "browser": { "enabled": true },
      "openai": { "enabled": true },
      "file-transfer": { "enabled": true }
    }
  },
  "gateway": {
    "mode": "remote",
    "remote": { "url": "wss://<gateway>.tailnet:8443", "transport": "direct" }
  }
}

paired.json on gateway (verified post node install --force):

{
  "<node-id-hash>": {
    "displayName": "<node-display-name>",
    "platform": "win32",
    "version": "2026.5.3-1",
    "caps": ["browser", "file-transfer", "system"],
    "commands": ["browser.proxy", "dir.fetch", "dir.list", "file.fetch", "file.write", "system.run", "system.run.prepare", "system.which"]
  }
}

Repro is fully deterministic in my environment — every restart of the node host yields the same broken handshake.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions