fix: proot deps, custom gateway port, node command policy, Ollama (v2026.9.14)

Merged
MI
2mo ago
+2982-452

Summary

Fixes setup being completely broken for new installs, closes several long-standing issues, and brings the Android toolchain up to a version that actually compiles on current Flutter.

Every behavioural change was checked against the upstream OpenClaw docs, then verified on a real device (Android 15, arm64), not just compiled.


The headline bug: setup could never work

On a fresh install, setup failed immediately with:

Setup failed: PlatformException(PROOT_ERROR, Command failed (exit code 1):
CANNOT LINK EXECUTABLE ".../lib/arm64/libproot.so": library "libandroid-shmem.so" not found

Termux's proot declares:

Package: proot
Version: 5.1.107.92
Depends: libandroid-shmem, libtalloc

scripts/fetch-proot-binaries.sh only fetched libtalloc, so the APK shipped a proot binary the dynamic linker could never load. Nothing downstream of it could run: no rootfs configuration, no Node.js, no OpenClaw. This is a plausible root cause for the recent wave of "setup failed" and "app is stuck" reports.

Fixed by fetching libandroid-shmem too, and by making the CI verification step fail (it previously only warned) when any of the four required libraries is missing or placeholder-sized, so a broken APK cannot be published again.

Verified on device:

$ libproot.so --version
proot 5.1.107.92
built-in accelerators: process_vm = yes, seccomp_filter = yes

Issues fixed

#124 Custom gateway port ignored

18789 was hard-coded in eight places: the health check, dashboard URL, token-URL regex, node WebSocket, native port probe, watchdog, notification, and the Termux CLI. Setting gateway.port left the app probing the wrong address and eventually declaring a healthy gateway dead.

Upstream precedence is --port > OPENCLAW_GATEWAY_PORT > gateway.port > 18789. A new GatewayConfig service resolves gateway.port from openclaw.json; GatewayService.kt reads it natively and passes --port explicitly for non-default ports. Token URLs are matched on any port now, so a custom port is auto-detected from gateway output.

#81 and #95 Node commands were never authorised

The app wrote gateway.nodes.allowCommands / denyCommands. Those keys do not exist upstream. The canonical keys are:

{ gateway: { nodes: { commands: { allow: [...], deny: [] } } } }

So every classified command (camera.snap, screen.record, and so on) had been silently unauthorised since the feature shipped, which is exactly why "how do I activate the camera" and "openclaw nodes list is empty" kept coming up. Now written under the correct keys, the bogus legacy keys are deleted from existing configs, and gateway.nodes.pairing.autoApproveLocal is set so the in-app node pairs without manual approval.

#133 npm install -g openclaw fails with ENOTEMPTY

An interrupted install leaves /usr/local/lib/node_modules/openclaw plus .openclaw-* staging dirs behind, and npm then fails to stage over them. Those are now cleared before installing, with a retry after a hard cache clean. Node.js also moves 22.14.0 to 22.23.2, satisfying undici's engines.node >= 22.19.0 (the EBADENGINE warning in that report).

#99 npm package lagged the repo

lib/index.js hard-coded VERSION = '1.7.3' while the package was 1.8.7. The version is now read from package.json, a new npm-publish.yml workflow publishes when the version changes, and tests assert package.json, pubspec.yaml and constants.dart agree so they cannot drift again.

#125 "app is stuck"

The splash screen ran the whole repair path inline, a Node.js download plus npm install -g openclaw with a 30 minute timeout, behind a static spinner, with no progress, no notification and no foreground service. The app looked frozen and Android could kill it mid-install, losing all the work.

Repair now runs in the setup wizard via BootstrapService.runRepair(), with step progress, a progress notification, and the wake-lock-holding foreground service. It also skips the ~500 MB rootfs download, since the rootfs is already present.

#107 SSH docs and hardening

Documented the actual setup order and every default with its rationale. sshd binds 0.0.0.0 (needed for LAN access, #61), so it now performs a pre-flight check for a real root password hash in /etc/shadow and refuses to start otherwise, instead of exposing a passwordless root shell. PermitEmptyPasswords no is pinned explicitly.

#117 Ollama support

Added, following the upstream provider docs exactly:

  • No /v1 in the base URL. That path selects OpenAI-compatible mode where tool calling is unreliable. A trailing /v1 is stripped automatically.
  • api: "ollama" pinned so native tool calling is guaranteed.
  • No API key required (a placeholder is written for availability checks).
  • timeoutSeconds: 300 for cold local models, and provider-qualified refs (ollama/qwen3:8b).
  • Editable base URL for LAN hosts or Ollama Cloud.

Gateway token

The gateway had no gateway.auth.token at all on a fresh setup, so the Control UI opened on a manual "Gateway Token" prompt and the node had no credential to present.

The config patch now guarantees a persistent token (32 random bytes, hex) before each start, so the tokenised URL works again:

http://localhost:18789/#token=8da9c1eb44f7e1525f9eb9fb6a0d320fe44f7132235fc9dca465e5f5f2420eab

It never overwrites an existing credential and leaves password, trusted-proxy and none auth setups untouched, because upstream fails startup when both a token and password are set without an explicit mode.

The node uses the same token. It already preferred gateway.auth.token from config, and its cached token is now cleared on gateway restart so a rotated token cannot cause TOKEN_INVALID reconnect loops (#94).

Verified on device: Gateway Running, tokenised URL shown with copy and open buttons, Web Dashboard card showing Token: 8da9c1eb..., and Node "Connected to gateway".


Performance

Setup wall-time, which is the substance of #125 and #111:

  • Node.js download now overlaps apt. It previously waited for apt to finish, making two slow phases fully serial.
  • python3 make g++ are no longer installed up front. Several hundred MB and a long dpkg run under proot, needed only when npm has to compile a native addon. Now installed by a fallback only if the install actually fails.
  • apt-get update -o Acquire::Languages=none skips translation indexes nobody reads; Acquire::Retries=3 helps on flaky mobile data.
  • npm runs with --omit=dev --no-audit --no-fund.
  • The CLI path drops a blanket apt upgrade -y on a freshly downloaded rootfs.
  • getInstallStatus({ skipProotProbe }) avoids repeated 30 second proot probes during setup.

Runtime:

  • Gateway log pipeline was O(n squared). Every line did [..._state.logs, log], a full copy of up to 500 entries, and pushed a new state, rebuilding the log UI once per line. --verbose emits thousands of lines. Replaced with a bounded ring buffer (O(1) amortised) and a 100 ms coalesced flush, so rebuilds are bounded by time rather than line count. Token detection stays synchronous so the dashboard URL is never delayed.

I could not measure the setup improvement end to end; that needs a full ~500 MB install run on a device.


Security

  • Node private key and tokens were in cloud backup. FlutterSharedPreferences.xml holds the node Ed25519 private key, the paired device token and the gateway auth token, and both backup_rules.xml and data_extraction_rules.xml included sharedpref wholesale. That file is now excluded from cloud backup and device transfer.
  • Settings snapshots leaked tokens. The export writes to shared storage (Downloads), where any app with storage access can read it. Device and gateway tokens are now omitted from both snapshot paths.
  • Pairing code validated against ^[A-Za-z0-9_-]{4,32}$ before being interpolated into the auto-approve shell command; the value arrives from the gateway over the network.
  • sshd refuses to start without a root password (above).

Toolchain

The project could not build on any current Flutter: the Gradle wrapper was 8.3, below Flutter's 8.7 minimum. Raising CI's Flutter version alone would have broken the build, so:

BeforeAfter
Gradle8.38.14.3
AGP8.1.08.11.1
Kotlin1.9.02.2.20
compileSdk3536
CI Flutter3.24.03.44.8

AGP stays on 8.x deliberately, because AGP 9 removes the legacy DSL this project uses. ndkVersion was added to the plugin subproject defaults, without which the jni plugin fails with Parameter specified as non-null is null: LibraryExtension.setNdkVersion.

CardTheme and DialogTheme migrated to CardThemeData and DialogThemeData, clearing 4 pre-existing analyzer errors.


Tests and project rules

The repo had no Dart tests and flutter analyze ran with continue-on-error: true, so analyzer regressions could not fail the build.

  • New flutter_app/test/, 25 tests covering port resolution, token capture (including the wrapped-TUI-box reconstruction that has broken repeatedly), the generated token's shape, and the provider contract.
  • npm suite expanded from 11 to 18 tests, including version-consistency guards and a style guard.
  • CI now runs flutter test and no longer swallows flutter analyze failures.
  • New AGENTS.md documents the writing style, the Windows/PowerShell environment, build and verify commands, the proot binary requirement, and the hard rules (do not rename the application ID, never delete outside filesDir, match upstream config keys, do not hard-code port 18789, keep versions in sync).
  • All em dashes and en dashes were replaced with ASCII hyphens across 24 files, with a test that fails if any are reintroduced. Box-drawing characters in the README diagram and the _boxDrawing regexes are untouched.

Verification

CheckResult
flutter analyze0 errors (5 pre-existing deprecation infos)
flutter test25/25 pass
npm test18/18 pass
npx eslint .clean
flutter build apk --releasesucceeds (43.5 MB)
adb install -rSuccess
On deviceGateway Running on 127.0.0.1:18789, Node Connected, tokenised URL present, no FATAL in logcat

netstat on the device, after setup completed through the full proot to Node to OpenClaw path:

tcp  0  0  127.0.0.1:18789  0.0.0.0:*  LISTEN

Docs

  • README capability table corrected to 9 capabilities / 21 commands. battery.status and the five serial.* commands were entirely undocumented. Plus new Gateway Port, SSH and Ollama sections, and a corrected structure tree.
  • Privacy policy gained a Node Device Capabilities section disclosing that camera, location, screen and sensor data can reach your configured AI provider, and now lists the hardware permissions the node requests.
  • All NextGenX references removed from the app, README, privacy policy and release assets.

Notes for the reviewer

  • The com.nxg.openclawproot application ID was deliberately left alone. Renaming it would break updates for every existing user.
  • flutter_app/pubspec.lock is now committed; the CI cache key already referenced it.
  • Generated build files (gradlew, gradle-wrapper.jar, GeneratedPluginRegistrant.java, .flutter-plugins-dependencies) are gitignored rather than committed.
  • Lowering minSdk to 28 for Android 9 (#100) was not attempted, since it cannot be verified without a device on that API level.

Summary by CodeRabbit

  • New Features

    • Added Ollama support with configurable local provider URLs.
    • Added configurable gateway ports instead of requiring port 18789.
    • Added repair mode for incomplete or failed installations.
    • Added Battery and Serial node capabilities.
    • Added project links for issues, releases, and upstream OpenClaw resources.
  • Security

    • Sensitive credentials are excluded from backups and exported settings snapshots.
    • SSH startup now requires a root password.
    • Pairing-code validation and gateway token handling were strengthened.
  • Bug Fixes

    • Improved installation recovery, package setup reliability, and custom-port dashboard connectivity.
    • Updated Android and Flutter tooling for improved compatibility.