Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9913248
chore: scaffold multihost branch — extension dirs, package.jsons, tsc…
damoahdominic Mar 20, 2026
7ddd79b
feat(multihost): add shared host types + adapter extension scaffolds
damoahdominic Mar 20, 2026
b38bca4
feat(multihost): HostRegistry, HostManager, status bar, tree provider…
damoahdominic Mar 20, 2026
e342e89
feat(multihost): openclaw-local extension — LocalHostAdapter + LocalH…
damoahdominic Mar 20, 2026
1f09951
feat(multihost): openclaw-docker extension + openclaw-ssh stub
damoahdominic Mar 20, 2026
c5a28cd
fix(multihost): correct adapter extension main paths after TS rootDir…
damoahdominic Mar 20, 2026
399373c
feat(multihost): Phase 2b — home.ts surgical refactor to HostConnection
damoahdominic Mar 20, 2026
fa70f41
feat: extract StatusPanelController + show full status panel in adapt…
damoahdominic Mar 22, 2026
da98b4b
feat(docker): auto-configure occ-legacy on Docker install, skip API k…
damoahdominic Mar 22, 2026
fa5f149
feat(docker): rewrite setup wizard to use official openclaw image
damoahdominic Mar 22, 2026
1f370f2
feat: smart host routing + dual status panel titles + hosts overview
damoahdominic Mar 22, 2026
11e120e
fix(docker): show full status panel instead of install screen
damoahdominic Mar 22, 2026
c20d228
feat: window-level host binding — one host per VS Code window
damoahdominic Mar 22, 2026
26d6184
fix: MultiHost Docker panel — eliminate setActiveHost race causing ho…
damoahdominic Mar 26, 2026
b739113
chore: add package-lock.json files for openclaw extensions
FletcherFrimpong Mar 26, 2026
3e4cdf7
security: fix XSS, CSP, command injection, and dependency vulnerabili…
FletcherFrimpong Mar 26, 2026
4bdb444
security: migrate JWT from globalState to SecretStorage (OS keychain)
FletcherFrimpong Mar 26, 2026
58a1669
security: verify openclaw package integrity before install
FletcherFrimpong Mar 26, 2026
f00e771
fix: fetch download URLs client-side to fix stale versions on static …
FletcherFrimpong Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions apps/editor/extensions/openclaw-docker/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions apps/editor/extensions/openclaw-docker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "openclaw-docker",
"displayName": "OpenClaw Docker",
"description": "Manage OpenClaw inside Docker containers",
"version": "1.0.0",
"publisher": "openclaw",
"license": "MIT",
"engines": { "vscode": "^1.85.0" },
"extensionDependencies": ["openclaw.home"],
"categories": ["Other"],
"activationEvents": ["onStartupFinished"],
"main": "./out/openclaw-docker/src/extension",
"contributes": {
"commands": [{ "command": "openclaw.host.setup.docker", "title": "OpenClaw: Set up Docker" }]
},
"scripts": {
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.85.0",
"typescript": "^5.3.0"
}
}
219 changes: 219 additions & 0 deletions apps/editor/extensions/openclaw-docker/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import * as vscode from 'vscode';
import * as cp from 'child_process';
import type {
HostAdapter,
HostType,
HostConnection,
HostConnectionConfig,
DockerConnection,
TestResult,
DiscoveredHost,
ConfigField,
ConfigValidationResult,
} from '../../openclaw/src/hosts/types';
import { DockerHostConnection } from './connection';
import { dockerPreflight } from './preflight';

// ─────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────

function listRunningContainers(): Array<{ id: string; name: string; image: string }> {
try {
const result = cp.spawnSync(
'docker',
['ps', '--format', '{{.ID}}\t{{.Names}}\t{{.Image}}'],
{ timeout: 6000, windowsHide: true },
);
if (result.status !== 0) { return []; }
return result.stdout
.toString()
.trim()
.split('\n')
.filter(Boolean)
.map(line => {
const [id, name, image] = line.split('\t');
return { id: id ?? '', name: name ?? '', image: image ?? '' };
});
} catch {
return [];
}
}

async function resolveContainerId(config: DockerConnection): Promise<string> {
if (config.containerId) { return config.containerId; }

if (config.containerLabel) {
const result = cp.spawnSync(
'docker',
['ps', '--filter', `name=${config.containerLabel}`, '--format', '{{.ID}}'],
{ timeout: 6000, windowsHide: true },
);
if (result.status === 0) {
const id = result.stdout.toString().trim().split('\n')[0];
if (id) { return id; }
}
}

if (config.composeService) {
const args = config.composeFile
? ['-f', config.composeFile, 'ps', '-q', config.composeService]
: ['ps', '-q', config.composeService];
const result = cp.spawnSync('docker', ['compose', ...args], {
timeout: 6000,
windowsHide: true,
});
if (result.status === 0) {
const id = result.stdout.toString().trim().split('\n')[0];
if (id) { return id; }
}
}

throw new Error(
'Could not resolve Docker container ID. Specify containerId, containerLabel, or composeService.',
);
}

// ─────────────────────────────────────────────
// DockerHostAdapter
// ─────────────────────────────────────────────

export class DockerHostAdapter implements HostAdapter {
readonly type: HostType = 'docker';
readonly displayName = 'Docker Container';
readonly icon = new vscode.ThemeIcon('package');

async discover(): Promise<DiscoveredHost[]> {
const preflight = await dockerPreflight();
if (!preflight.ok) { return []; }

return listRunningContainers().map(c => ({
suggestedId: `docker:${c.id.slice(0, 12)}`,
suggestedLabel: c.name || c.id.slice(0, 12),
connection: {
type: 'docker',
containerId: c.id,
containerLabel: c.name,
} as DockerConnection,
metadata: { image: c.image },
}));
}

async connect(config: HostConnectionConfig): Promise<HostConnection> {
const dockerConfig = config as DockerConnection;

// Run preflight before attempting connection
const preflight = await dockerPreflight();
if (!preflight.ok) {
throw new Error(preflight.remedy);
}

const containerId = await resolveContainerId(dockerConfig);
return new DockerHostConnection(dockerConfig, containerId);
}

async testConnection(config: HostConnectionConfig): Promise<TestResult> {
const dockerConfig = config as DockerConnection;

// 1. Docker daemon preflight
const preflight = await dockerPreflight();
if (!preflight.ok) {
return {
success: false,
message: preflight.remedy,
details: { os: process.platform },
};
}

// 2. Resolve container
let containerId: string;
try {
containerId = await resolveContainerId(dockerConfig);
} catch (err) {
return {
success: false,
message: String(err),
details: { os: process.platform },
};
}

// 3. Test CLI inside container
const conn = new DockerHostConnection(dockerConfig, containerId);
try {
const cliCheck = await conn.testOpenClawCli();
const gwStatus = cliCheck.ok ? await conn.gatewayHealthCheck() : undefined;
return {
success: cliCheck.ok,
message: cliCheck.ok
? `OpenClaw CLI found in container ${containerId.slice(0, 12)} (${cliCheck.output ?? ''})`
: `Container ${containerId.slice(0, 12)}: ${cliCheck.error ?? 'CLI not found'}`,
details: {
openclawInstalled: cliCheck.ok,
openclawVersion: cliCheck.output,
gatewayRunning: gwStatus?.state === 'running',
os: process.platform,
hostname: containerId.slice(0, 12),
},
};
} finally {
conn.dispose();
}
}

getConfigFields(): ConfigField[] {
return [
{
id: 'containerId',
label: 'Container ID or Name',
type: 'text',
placeholder: 'e.g. my-openclaw or abc123def456',
required: false,
},
{
id: 'composeService',
label: 'Docker Compose Service',
type: 'text',
placeholder: 'e.g. openclaw (from docker-compose.yml)',
required: false,
},
{
id: 'composeFile',
label: 'Compose File Path',
type: 'text',
placeholder: 'Leave blank for default docker-compose.yml',
required: false,
},
{
id: 'dockerHost',
label: 'Docker Host (DOCKER_HOST)',
type: 'text',
placeholder: 'Leave blank for local socket',
required: false,
},
{
id: 'shell',
label: 'Shell inside container',
type: 'text',
placeholder: '/bin/sh (default) or /bin/bash',
defaultValue: '/bin/sh',
required: false,
},
];
}

validateConfig(config: HostConnectionConfig): ConfigValidationResult {
const dockerConfig = config as DockerConnection;
if (!dockerConfig.containerId && !dockerConfig.containerLabel && !dockerConfig.composeService) {
return {
valid: false,
errors: [
{
fieldId: 'containerId',
message: 'Provide at least one of: Container ID/Name or Compose Service.',
},
],
};
}
return { valid: true };
}
}
Loading
Loading