feat: multihost support — local, Docker, SSH adapter architecture#53
Merged
damoahdominic merged 19 commits intodamoahdominic:mainfrom Mar 27, 2026
Merged
feat: multihost support — local, Docker, SSH adapter architecture#53damoahdominic merged 19 commits intodamoahdominic:mainfrom
damoahdominic merged 19 commits intodamoahdominic:mainfrom
Conversation
…onfigs Creates openclaw-local, openclaw-docker, openclaw-ssh extension scaffolds and new src/hosts, src/ui, src/api directories in core extension. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- apps/editor/extensions/openclaw/src/hosts/types.ts: complete shared interface definitions — HostAdapter, HostConnection, HostEntry, HostsFile, HostCache, OpenClawCoreAPI, all connection configs (Local/Docker/SSH/Cloud), LogFn, ExecOpts/Result, GatewayStatus, etc. - apps/editor/extensions/openclaw-local/: new standalone extension stub (package.json + tsconfig.json) - apps/editor/extensions/openclaw-docker/: new standalone extension stub - apps/editor/extensions/openclaw-ssh/: new standalone extension stub (preview, 0.1.0) Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…, API export - hosts/registry.ts: reads/writes ~/.occ/hosts.json, seeds local default, file-watches for external changes, CRUD for hosts + activeHostId - hosts/manager.ts: implements OpenClawCoreAPI; owns live HostConnection map; registers adapters, connects persisted hosts, drives status events - hosts/statusbar.ts: status bar item showing active host with icon - hosts/tree.ts: Tree view provider listing all hosts (openclaw.hosts view) - extension.ts: bootstraps registry + manager, exports OpenClawCoreAPI from activate(), registers openclaw.pickHost / setActiveHost / refreshHost commands - package.json: adds views (openclaw.hosts in explorer), 3 new commands Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…ostConnection LocalHostConnection implements full HostConnection interface for the local machine: - exec / execStream: child_process.spawn with full opts support - Filesystem: readFile/writeFile/exists/mkdir/stat via Node fs - CLI detection: user config → which/where → well-known paths (darwin/win32/linux) - installCli: curl|bash on unix, irm|iex on windows - readConfig/writeConfig: ~/.openclaw/openclaw.json - gatewayHealthCheck: `openclaw gateway status --json`, falls back to string parse - gatewayStart/Stop/Restart, runSetup (openclaw onboard) LocalHostAdapter: discovers one local host, testConnection returns CLI + gateway state. openclaw-local/extension.ts: grabs OpenClawCoreAPI from openclaw.home exports, registers LocalHostAdapter, adds disposable to subscriptions. tsconfig.json: removed rootDir on all three adapter extensions so cross-extension relative imports (../../openclaw/src/hosts/types) compile correctly. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
openclaw-docker: - preflight.ts: three-state Docker health check (cli_missing / daemon_down / permission_denied) with platform-specific remedies for darwin (OrbStack), win32 (Docker Desktop + WSL2), linux (systemd + apt/dnf) - connection.ts: DockerHostConnection — all HostConnection methods via `docker exec`; writeFile via stdin+tee; portMapping aware gatewayHealthCheck - adapter.ts: DockerHostAdapter — discovers running containers via `docker ps`; resolves containerId from label or compose service; testConnection runs preflight + CLI check inside container; full getConfigFields / validateConfig - extension.ts: registers DockerHostAdapter against openclaw.home exports openclaw-ssh: - src/extension.ts: activation stub (logs "coming soon", no-op) All four extensions compile cleanly with zero TypeScript errors. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
… inference TypeScript infers the rootDir as apps/editor/extensions/ (common ancestor) when import-type references span across extension directories. This causes output files to land in out/openclaw-local/src/ and out/openclaw-docker/src/ rather than out/. Fix: update "main" in each adapter's package.json to match the actual compiled path: - openclaw-local: ./out/openclaw-local/src/extension - openclaw-docker: ./out/openclaw-docker/src/extension - openclaw-ssh: ./out/extension (no cross-extension imports, unaffected) All four extensions verified with clean builds. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
HomePanel now delegates all host I/O through a HostConnection interface,
making it host-agnostic and ready for Docker/SSH in future:
Key changes in home.ts:
- _host: HostConnection property (DefaultLocalHostConnection by default);
swapped to active host when HostManager fires onDidChangeActiveHost
- _buildExecEnv() → this._host.buildExecEnv() (eliminates 55-line method)
- _testOpenClawCli() → this._host.testOpenClawCli() (eliminates 103-line method)
- _findOpenClawPath() → this._host.findOpenClawPath() (eliminates 65-line method)
- _quickInstallCheck() → async, delegates to this._host.exists(configPath)
- _getConfiguredPort() → reads _cachedGatewayPort (updated async in _update())
- _update() → host.exists()/readConfig()/getConfigPath() for config detection
- _runSetup() → this._host.execStream() for openclaw onboard subprocess;
host.readConfig()/writeConfig() for openclaw.json patching;
host.exec() for Node.js version check
New files:
- hosts/localDefault.ts: DefaultLocalHostConnection — self-contained local
implementation within the core extension; no cross-extension source imports
- hosts/types.ts: added shell?: boolean to ExecOpts (needed for Windows .cmd shims)
- openclaw-local/connection.ts: propagate shell option to cp.spawn
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…er tabs - Extract _getHtml() from HomePanel into shared renderStatusHtml() in statusHtml.ts - Create StatusPanelController (statusController.ts) with all behavioral logic: gateway polling, version checks, sign-in/out, CASS/Better Memory setup, workspace file shortcuts — usable by any adapter panel - LocalSetupPanel: if OpenClaw already installed, show full status panel immediately instead of wizard; after setup success, show status panel in-tab instead of delegating to HomePanel - DockerSetupPanel: after configure success, show status panel in-tab instead of disposing the panel - Adapters now delegate all webview messages to StatusPanelController once the status panel is active Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…ey prompt Docker wizard now automatically onboards with the OCC free tier (occ-legacy model at occ.mba.sh/v1) after the CLI installs — no API key required. Users can sign in and change the model later from the status panel. - Remove step 4 provider grid / API key form entirely - After CLI install, immediately trigger onboard with occ-legacy flags - Patch openclaw.json in container to set correct model metadata - Write moltpilot-tier.json to the mounted host state dir Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Replace 4-step wizard (Docker Check → Container+Install → Configure → Status) with new 4-step flow: 1. Docker Check (docker info) 2. Pull Image (ghcr.io/openclaw/openclaw:latest + create state dir) 3. Onboard (one-shot docker run --rm with occ-legacy auto-config) 4. Launch Gateway (persistent container, port 18790:18789) - Volume mount: ~/Desktop/occ-state-dir:/home/node/.openclaw - Port mapping: 18790:18789 (avoids collision with local install) - Config path fixed to /home/node/.openclaw/openclaw.json (user 'node') - occ-legacy patched directly on host state dir after onboard Add gatewayHostPort() to HostConnection interface so StatusPanelController polls the host-side port (18790) instead of the container port (18789). Implemented in DockerHostConnection (returns portMappings.gateway) and DefaultLocalHostConnection (returns undefined / no remapping). Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Smart routing on openclaw.home command:
- Only Local installed → open LocalSetupPanel directly
- Only Docker running → open DockerSetupPanel directly
- Both installed → HomePanel with hosts overview
- Neither → HomePanel (install wizard)
Hosts overview (shown when both are active):
- Two cards: Local + Docker with live gateway status pills
(checking/running/stopped), port labels, polls every 5 seconds
- Clicking a card opens the respective setup panel
Panel tab titles updated when status panel is active:
- LocalSetupPanel → "OCC Home {Local:18789}" (reads port from openclaw.json)
- DockerSetupPanel → "OCC Home {Docker:18790}"
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Two root causes fixed: 1. StatusPanelController: for non-local hosts dirExists was always checking local ~/.openclaw (doesn't exist for Docker-only users). Now uses isConfigured for non-local. isInstalled also simplified: non-local hosts are always considered installed (status panel is only opened after setup — if we're here, openclaw is in the image). 2. DockerSetupPanel: add _initHtml() with auto-detection (like LocalSetupPanel). On open, checks synchronously if occ-openclaw container is running, then verifies openclaw config/CLI inside it. If already set up → jumps directly to status panel. If not → shows the setup wizard. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Global state (workspaceState) tracks which host this window is bound to.
MoltPilot and any component can read it via occ.window.getHost command.
Commands registered:
occ.window.setHost — set { type, hostId, port, label }
occ.window.clearHost — remove binding
occ.window.getHost — read binding (returns null if unbound)
Routing (openclaw.home / startup):
- Bound window → routes directly to bound host panel (no picker)
- Unbound → existing detection logic (local/docker/both/neither)
Single-tab enforcement:
- HomePanel disposes itself before opening adapter panel (picker closes)
- Adapter panels set occ.window.setHost when status panel activates
Disconnect flow (... menu → "Disconnect host"):
- Clears occ.window.clearHost
- Disposes adapter panel
- Reopens host picker (openclaw.home)
- StatusPanelController now accepts optional onDisconnect callback
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…st-selector bounce
Removes addHost()/setActiveHost() from DockerSetupPanel._showStatusPanel().
Firing onDidChangeActiveHost was triggering HomePanel._update() which re-rendered
the hosts overview (bouncing user back to host selector). Also eliminates the
blocking dockerPreflight() spawnSync call that was piggybacked on addHost().
Other MultiHost changes included in this commit:
- statusController: debounced setActiveOpenClawWorkspaceFolder to prevent
concurrent .code-workspace writes ("File Modified Since" error)
- home.ts: read local port directly from disk in _getHostsOverviewHtml to
avoid _cachedGatewayPort contamination from Docker host
- home.ts: add "Best if:" context bullets on host selection cards
- DockerSetupPanel: show loading spinner immediately on open (fire-and-forget
_showStatusPanel) instead of blank panel
- DockerSetupPanel: _disposed guard to prevent double-dispose
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: FletcherFrimpong <[email protected]>
…ties - config.ts: remove unsafe-eval from Content Security Policy - statusController.ts: HTML-escape version strings before innerHTML injection - statusHtml.ts: sanitize maintainerName/URL before inserting into innerHTML - extension.ts: use imported cp module instead of require(); redact API keys from logs - openclaw-ssh/connection.ts: validate file paths to reject null bytes and non-absolute paths - status.ts: add localResourceRoots to restrict webview resource loading - apps/web: upgrade Next.js to 16.2.1 to fix CSRF bypass and HTTP smuggling CVEs Co-Authored-By: FletcherFrimpong <[email protected]>
JWT was stored in plaintext globalState (a JSON file on disk), readable by any process running as the same OS user or any VS Code extension. Changes: - All reads: context.globalState.get(OCC_JWT_KEY) → context.secrets.get() - All writes: context.globalState.update() → context.secrets.store/delete() - One-time migration on activate: moves existing JWT from globalState to SecretStorage, then deletes plaintext copy - Smoke test no longer logs any part of the JWT - Deep-link URI handler (occode://auth) stores token in SecretStorage JWT is now encrypted at rest via OS keychain (Keychain on macOS, Credential Manager on Windows, libsecret on Linux). Co-Authored-By: FletcherFrimpong <[email protected]>
Before running npm install, OCC now: 1. Fetches package metadata from registry.npmjs.org 2. Downloads the tarball to a temp file 3. Verifies SHA-512 hash against dist.integrity (SRI format) 4. Only installs from the verified local file — no re-download All three install paths are covered: - Main npm install path - Sudo retry path (re-downloads and re-verifies) - nvm fallback path If the hash does not match, installation is aborted with a clear error message. Temp files are always cleaned up after install. Protects against: MITM attacks, CDN compromise, corrupted downloads. Co-Authored-By: FletcherFrimpong <[email protected]>
…export The site uses Next.js static export (output: "export"), which means server-side fetches only run at build time. Moving the GitHub releases API fetch to a client-side useEffect ensures users always get the latest release download URLs at runtime.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
HostRegistry(~/.occ/hosts.json),HostManager, andOpenClawCoreAPIfor adapter registrationopenclaw-localandopenclaw-dockeras dedicated extensions with full connection implementationsSecurity Fixes
✅ JWT migrated from
globalState→SecretStorage(OS keychain)JWT was previously stored in plaintext on disk, readable by any VS Code extension or OS process. Now encrypted at rest via macOS Keychain / Windows Credential Manager / libsecret.
On first launch after this update, the extension automatically moves any existing JWT from
globalStateintoSecretStoragethen deletes the plaintext copy — existing users will not need to sign in again.Protects against:
git add .✅ npm package integrity verification before install
Before running
npm install, OCC now:registry.npmjs.orgdist.integrity(SRI format)All three install paths covered: main, sudo retry, and nvm fallback. If the hash does not match, installation is aborted with a clear error message.
Protects against: MITM attacks, CDN compromise, corrupted downloads.
✅ Additional security fixes
unsafe-evalfrom Content Security Policy in config panelinnerHTMLto prevent XSS from a compromised npm registryinnerHTMLrequire('child_process')with already-importedcpmodulelocalResourceRootsto restrict webview resource loadingTest plan
🤖 Generated with Claude Code