AddedAndroid branch in `api/src/instances/instances.service.ts` (`getSetupScript`): detects Termux via `uname -o` / `$ANDROID_ROOT` / `$TERMUX_VERSION`, skips sudo/SSM/WireGuard/metrics-exporters, runs `pkg install` for nodejs, jq, curl, tar, wget, openssl-tool, lsof, plus tur-repo + cloudflared. Reports `platformType: "Android"`, `environment: "android"` in the setup callback.
Added`tropic-runner` daemon, written by the setup script as `~/.tropic-runner/runner.js` and started via `nohup`. Bearer-token auth on `127.0.0.1:18790`, exposes `POST /_tropic/exec` (returns `{stdout, stderr, code, signal}`) and `GET /_tropic/health`. Token is generated at setup time and posted back to the API in the callback alongside `gatewayToken`.
AddedCloudflare Tunnel ingress rule `^/_tropic/` → `localhost:18790` in `cloudflare-tunnel.service.ts`. Existing tunnels reconcile on next API boot via `reconcileAllTunnelsIngress`.
Added`ManagedInstance.runnerToken` column (migration `20260502000000_add_runner_token`) plus wiring in the setup callback DTO/handler.
Added`InstanceExecService` (`api/src/instances/instance-exec.service.ts`) with `runShell()` (waits, returns `{stdout, stderr, status, exitCode}`) and `runShellAsync()` (fire-and-forget). Branches on `metadata.platformType === "Android"`: HTTPS POST through the runner via `<tunnelUrl>/_tropic/exec` for Android, otherwise existing SSM `SendCommandCommand` + `GetCommandInvocationCommand` poll loop. Lives in its own `InstanceExecModule` to break the cycle that would form between `InstancesModule` and `CredentialsModule`.
Added`OcShellHelper.userOpsPreamble(instanceType, ocHome)` extends `pathPreamble` with a sudo-detection line that defines `$AS_OC_USER` — resolves to `sudo -u $OC_USER` on EC2 (root → ubuntu) or empty on Android (already running as the user).
AddedPlugin tarball delivery for local instances: `GET /instances/plugins/:token` streams the four Tropic plugins (sondera, tropic-telemetry, policy-check, tropic-memory) so local-instance setup can install them the same way the EC2 AMI build does. Plugin sources copied into the API runtime image via `Dockerfile`.
Changed`agents.service.ts`: `pushAgentToVm` and `pushPolicyToVm` now take an `instance` object (replacing loose `region`, `instanceId`, `instanceType`, `ocHome` args) and route through `instanceExec.runShell` / `runShellAsync`. Five call sites updated across `agents.service.ts` and `vm.service.ts`.
Changed`credentials.service.ts`: API key sync, deletion, and SSH pubkey push routed through `InstanceExecService`. Hardcoded `/home/ubuntu/.openclaw/.env` and `sudo -u ubuntu` removed in favour of `OcShellHelper.userOpsPreamble()` which sets `$OC_DIR` and `$AS_OC_USER`. Gateway restart switched from `systemctl restart openclaw-gateway` to `openclaw gateway restart` so it works cross-platform.
Changed`settings.service.ts`: 8 SSM call sites collapsed onto `instanceExec.runShell`/`runShellAsync`. `syncOpenclawModelToVm` and `syncSkillsToVm` now use `$OC_DIR` paths so they work on Termux. The chmod-based restriction helpers (`applyOsRestrictionsToVm`, `applyBrowserRestrictions`, `applyCodeExecutionRestrictions`) early-return on Android since they target `/usr/bin/*` system binaries that don't exist on Termux. Removed the now-dead `waitForCommandComplete` helper, `ssmClients` map, and `@aws-sdk/client-ssm` imports.
FixedOn Termux, `eval echo "~$REAL_USER"` does not expand because Android UIDs (e.g. `u0_a123`) have no `/etc/passwd` entry. The literal `~u0_a123` then propagated through `OC_HOME`, `OC_CONFIG`, the `[ -f "$OC_CONFIG" ]` gate, and silently skipped the entire jq + gateway-restart + runner-install + callback block. Setup script now uses `$HOME` directly when `SUDO_USER` is unset.
FixedTermux `npm install -g` copies binaries instead of symlinking. The copied `openclaw` bin at `$PREFIX/bin/openclaw` resolves `./dist/entry.js` (via `import.meta.url`) against `/usr/bin/`, throwing `missing dist/entry.(m)js`. Setup script now replaces the copy with a symlink to `$PREFIX/lib/node_modules/openclaw/openclaw.mjs`.
FixedTermux has no `/usr/bin/env`, so npm-installed shebangs `#!/usr/bin/env node` were rejected with "bad interpreter". Setup script now runs `termux-fix-shebang` on the openclaw bin (idempotent).
FixedTermux `/tmp` is not writable. Three temp-file writes (`/tmp/oc-setup.tmp`, `/tmp/oc-plugins.tmp`, `/tmp/tropic-plugins-*.tar.gz`) failed silently. Setup script now uses `${TMPDIR:-/tmp}` everywhere on the Android path.
FixedTermux has no `which` command, so `OC_BIN=$(... which openclaw ...)` returned empty and skipped the gateway restart. Replaced `which` with the POSIX builtin `command -v`.
FixedGateway port-bind poll after `nohup openclaw gateway run` was too short (2s) — plugin init can push cold start past 5s, leading to a false-negative "Warning: Gateway may need manual restart". Extended to 12 × 1s polls.