@swarmclawai/swarmclaw 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -10
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +16 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/route.ts +21 -1
- package/src/app/api/chats/route.ts +13 -1
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
- package/src/app/api/external-agents/[id]/route.ts +38 -6
- package/src/app/api/external-agents/route.ts +17 -1
- package/src/app/api/gateways/[id]/health/route.ts +8 -0
- package/src/app/api/gateways/[id]/route.ts +53 -1
- package/src/app/api/gateways/route.ts +53 -0
- package/src/app/api/openclaw/deploy/route.ts +139 -0
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/cli/index.js +40 -0
- package/src/cli/index.test.js +68 -0
- package/src/cli/spec.js +60 -0
- package/src/components/agents/agent-sheet.tsx +281 -33
- package/src/components/auth/setup-wizard.tsx +75 -2
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-header.tsx +4 -0
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/gateways/gateway-sheet.tsx +140 -8
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +221 -17
- package/src/components/shared/settings/section-capability-policy.tsx +38 -0
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/tasks/approvals-panel.tsx +177 -18
- package/src/components/tasks/task-board.tsx +137 -23
- package/src/components/tasks/task-card.tsx +29 -0
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/lib/server/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- package/src/lib/server/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +33 -3
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.ts +99 -74
- package/src/lib/server/daemon-state.ts +83 -46
- package/src/lib/server/elevenlabs.test.ts +59 -1
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/openclaw-deploy.test.ts +8 -0
- package/src/lib/server/openclaw-deploy.ts +679 -19
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +11 -0
- package/src/lib/server/plugins.ts +6 -1
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-followups.test.ts +147 -2
- package/src/lib/server/queue.ts +278 -8
- package/src/lib/server/session-run-manager.ts +31 -0
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.ts +26 -1
- package/src/lib/server/session-tools/context.ts +5 -0
- package/src/lib/server/session-tools/crud.ts +265 -76
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +38 -2
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.ts +14 -2
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
- package/src/lib/server/session-tools/web.ts +153 -6
- package/src/lib/server/stream-agent-chat.test.ts +27 -2
- package/src/lib/server/stream-agent-chat.ts +104 -30
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +269 -0
- package/src/lib/setup-defaults.ts +2 -2
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +104 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
|
+
import { promises as fs } from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
3
5
|
import path from 'node:path'
|
|
4
6
|
import {
|
|
5
7
|
getManagedProcess,
|
|
6
8
|
killManagedProcess,
|
|
7
9
|
removeManagedProcess,
|
|
8
10
|
startManagedProcess,
|
|
11
|
+
type ProcessStatus,
|
|
9
12
|
} from './process-manager'
|
|
10
13
|
import { normalizeOpenClawEndpoint, deriveOpenClawWsUrl } from '@/lib/openclaw-endpoint'
|
|
14
|
+
import { probeOpenClawHealth, type OpenClawHealthResult } from './openclaw-health'
|
|
11
15
|
|
|
12
16
|
export type OpenClawRemoteDeployTemplate = 'docker' | 'render' | 'fly' | 'railway'
|
|
13
17
|
export type OpenClawRemoteDeployProvider =
|
|
@@ -20,6 +24,8 @@ export type OpenClawRemoteDeployProvider =
|
|
|
20
24
|
| 'azure'
|
|
21
25
|
| 'oci'
|
|
22
26
|
| 'generic'
|
|
27
|
+
export type OpenClawUseCaseTemplate = 'local-dev' | 'single-vps' | 'private-tailnet' | 'browser-heavy' | 'team-control'
|
|
28
|
+
export type OpenClawExposurePreset = 'private-lan' | 'tailscale' | 'caddy' | 'nginx' | 'ssh-tunnel'
|
|
23
29
|
|
|
24
30
|
export interface OpenClawLocalDeployStatus {
|
|
25
31
|
running: boolean
|
|
@@ -36,6 +42,22 @@ export interface OpenClawLocalDeployStatus {
|
|
|
36
42
|
installCommand: string
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
export interface OpenClawRemoteDeployStatus {
|
|
46
|
+
active: boolean
|
|
47
|
+
processId: string | null
|
|
48
|
+
pid: number | null
|
|
49
|
+
action: string | null
|
|
50
|
+
target: string | null
|
|
51
|
+
startedAt: number | null
|
|
52
|
+
status: ProcessStatus | 'idle'
|
|
53
|
+
exitCode: number | null
|
|
54
|
+
tail: string
|
|
55
|
+
lastError: string | null
|
|
56
|
+
lastSummary: string | null
|
|
57
|
+
lastCommandPreview: string | null
|
|
58
|
+
lastBackupPath: string | null
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
export interface OpenClawDeployBundleFile {
|
|
40
62
|
name: string
|
|
41
63
|
language: 'bash' | 'yaml' | 'env' | 'toml' | 'text'
|
|
@@ -46,6 +68,8 @@ export interface OpenClawDeployBundle {
|
|
|
46
68
|
template: OpenClawRemoteDeployTemplate
|
|
47
69
|
provider: OpenClawRemoteDeployProvider
|
|
48
70
|
providerLabel: string
|
|
71
|
+
useCase: OpenClawUseCaseTemplate
|
|
72
|
+
exposure: OpenClawExposurePreset
|
|
49
73
|
title: string
|
|
50
74
|
summary: string
|
|
51
75
|
endpoint: string
|
|
@@ -55,6 +79,24 @@ export interface OpenClawDeployBundle {
|
|
|
55
79
|
files: OpenClawDeployBundleFile[]
|
|
56
80
|
}
|
|
57
81
|
|
|
82
|
+
export interface OpenClawSshConfig {
|
|
83
|
+
host: string
|
|
84
|
+
user?: string | null
|
|
85
|
+
port?: number | null
|
|
86
|
+
keyPath?: string | null
|
|
87
|
+
targetDir?: string | null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface OpenClawRemoteCommandResult {
|
|
91
|
+
ok: boolean
|
|
92
|
+
started: boolean
|
|
93
|
+
processId?: string | null
|
|
94
|
+
summary: string
|
|
95
|
+
commandPreview: string
|
|
96
|
+
token?: string
|
|
97
|
+
bundle?: OpenClawDeployBundle
|
|
98
|
+
}
|
|
99
|
+
|
|
58
100
|
interface LocalRuntimeState {
|
|
59
101
|
processId: string | null
|
|
60
102
|
port: number
|
|
@@ -65,8 +107,20 @@ interface LocalRuntimeState {
|
|
|
65
107
|
lastError: string | null
|
|
66
108
|
}
|
|
67
109
|
|
|
110
|
+
interface RemoteRuntimeState {
|
|
111
|
+
processId: string | null
|
|
112
|
+
action: string | null
|
|
113
|
+
target: string | null
|
|
114
|
+
startedAt: number | null
|
|
115
|
+
lastError: string | null
|
|
116
|
+
lastSummary: string | null
|
|
117
|
+
lastCommandPreview: string | null
|
|
118
|
+
lastBackupPath: string | null
|
|
119
|
+
}
|
|
120
|
+
|
|
68
121
|
interface DeployRuntimeState {
|
|
69
122
|
local: LocalRuntimeState
|
|
123
|
+
remote: RemoteRuntimeState
|
|
70
124
|
}
|
|
71
125
|
|
|
72
126
|
interface RemoteProviderMeta {
|
|
@@ -77,6 +131,22 @@ interface RemoteProviderMeta {
|
|
|
77
131
|
summary: string
|
|
78
132
|
}
|
|
79
133
|
|
|
134
|
+
interface UseCaseMeta {
|
|
135
|
+
id: OpenClawUseCaseTemplate
|
|
136
|
+
label: string
|
|
137
|
+
summary: string
|
|
138
|
+
detail: string
|
|
139
|
+
defaultExposure: OpenClawExposurePreset
|
|
140
|
+
hostBind: string
|
|
141
|
+
nodeOptions: string | null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface ExposureMeta {
|
|
145
|
+
id: OpenClawExposurePreset
|
|
146
|
+
label: string
|
|
147
|
+
summary: string
|
|
148
|
+
}
|
|
149
|
+
|
|
80
150
|
const DEFAULT_LOCAL_PORT = 18789
|
|
81
151
|
const DEFAULT_REMOTE_PORT = 18789
|
|
82
152
|
const GLOBAL_KEY = '__swarmclaw_openclaw_deploy__' as const
|
|
@@ -147,6 +217,82 @@ const REMOTE_PROVIDER_META: Record<OpenClawRemoteDeployProvider, RemoteProviderM
|
|
|
147
217
|
},
|
|
148
218
|
}
|
|
149
219
|
|
|
220
|
+
const USE_CASE_META: Record<OpenClawUseCaseTemplate, UseCaseMeta> = {
|
|
221
|
+
'local-dev': {
|
|
222
|
+
id: 'local-dev',
|
|
223
|
+
label: 'Local Dev',
|
|
224
|
+
summary: 'Local-first OpenClaw control plane for testing and personal machines.',
|
|
225
|
+
detail: 'Binds to loopback with safe defaults so a single developer can stand up OpenClaw quickly.',
|
|
226
|
+
defaultExposure: 'private-lan',
|
|
227
|
+
hostBind: '127.0.0.1',
|
|
228
|
+
nodeOptions: null,
|
|
229
|
+
},
|
|
230
|
+
'single-vps': {
|
|
231
|
+
id: 'single-vps',
|
|
232
|
+
label: 'Single VPS',
|
|
233
|
+
summary: 'Balanced always-on control plane for one server and a small swarm.',
|
|
234
|
+
detail: 'Good default for Hetzner, DigitalOcean, Vultr, Linode, Lightsail, and generic Ubuntu VPS installs.',
|
|
235
|
+
defaultExposure: 'caddy',
|
|
236
|
+
hostBind: '0.0.0.0',
|
|
237
|
+
nodeOptions: null,
|
|
238
|
+
},
|
|
239
|
+
'private-tailnet': {
|
|
240
|
+
id: 'private-tailnet',
|
|
241
|
+
label: 'Private Tailnet',
|
|
242
|
+
summary: 'Keep the gateway off the public internet and expose it only through a trusted tailnet.',
|
|
243
|
+
detail: 'Uses loopback binding and pairs well with Tailscale or an SSH tunnel.',
|
|
244
|
+
defaultExposure: 'tailscale',
|
|
245
|
+
hostBind: '127.0.0.1',
|
|
246
|
+
nodeOptions: null,
|
|
247
|
+
},
|
|
248
|
+
'browser-heavy': {
|
|
249
|
+
id: 'browser-heavy',
|
|
250
|
+
label: 'Browser Heavy',
|
|
251
|
+
summary: 'Higher-memory defaults for browser tools and long-running automation nodes.',
|
|
252
|
+
detail: 'Raises Node memory limits and assumes a roomier VPS profile for browser-backed tasks.',
|
|
253
|
+
defaultExposure: 'caddy',
|
|
254
|
+
hostBind: '0.0.0.0',
|
|
255
|
+
nodeOptions: '--max-old-space-size=3072',
|
|
256
|
+
},
|
|
257
|
+
'team-control': {
|
|
258
|
+
id: 'team-control',
|
|
259
|
+
label: 'Team Control',
|
|
260
|
+
summary: 'Shared control plane defaults for a trusted team with backups and cleaner exposure choices.',
|
|
261
|
+
detail: 'Prioritizes predictable exposure and easier operator handoff across a team.',
|
|
262
|
+
defaultExposure: 'caddy',
|
|
263
|
+
hostBind: '0.0.0.0',
|
|
264
|
+
nodeOptions: '--max-old-space-size=2048',
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const EXPOSURE_META: Record<OpenClawExposurePreset, ExposureMeta> = {
|
|
269
|
+
'private-lan': {
|
|
270
|
+
id: 'private-lan',
|
|
271
|
+
label: 'Private LAN',
|
|
272
|
+
summary: 'Expose only on your LAN or through provider firewall rules.',
|
|
273
|
+
},
|
|
274
|
+
tailscale: {
|
|
275
|
+
id: 'tailscale',
|
|
276
|
+
label: 'Tailscale',
|
|
277
|
+
summary: 'Keep OpenClaw on loopback and publish it only over your Tailscale tailnet.',
|
|
278
|
+
},
|
|
279
|
+
caddy: {
|
|
280
|
+
id: 'caddy',
|
|
281
|
+
label: 'Caddy',
|
|
282
|
+
summary: 'Run a bundled reverse proxy that can terminate HTTPS and proxy the gateway safely.',
|
|
283
|
+
},
|
|
284
|
+
nginx: {
|
|
285
|
+
id: 'nginx',
|
|
286
|
+
label: 'Nginx',
|
|
287
|
+
summary: 'Use an Nginx reverse proxy for teams that already manage TLS or edge certificates.',
|
|
288
|
+
},
|
|
289
|
+
'ssh-tunnel': {
|
|
290
|
+
id: 'ssh-tunnel',
|
|
291
|
+
label: 'SSH Tunnel',
|
|
292
|
+
summary: 'Keep the gateway on loopback and access it through an SSH tunnel when needed.',
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
|
|
150
296
|
function getRuntimeState(): DeployRuntimeState {
|
|
151
297
|
const fallback: DeployRuntimeState = {
|
|
152
298
|
local: {
|
|
@@ -158,6 +304,16 @@ function getRuntimeState(): DeployRuntimeState {
|
|
|
158
304
|
startedAt: null,
|
|
159
305
|
lastError: null,
|
|
160
306
|
},
|
|
307
|
+
remote: {
|
|
308
|
+
processId: null,
|
|
309
|
+
action: null,
|
|
310
|
+
target: null,
|
|
311
|
+
startedAt: null,
|
|
312
|
+
lastError: null,
|
|
313
|
+
lastSummary: null,
|
|
314
|
+
lastCommandPreview: null,
|
|
315
|
+
lastBackupPath: null,
|
|
316
|
+
},
|
|
161
317
|
}
|
|
162
318
|
const globalState = globalThis as typeof globalThis & { [GLOBAL_KEY]?: DeployRuntimeState }
|
|
163
319
|
if (!globalState[GLOBAL_KEY]) {
|
|
@@ -228,6 +384,10 @@ function normalizeToken(value: unknown): string | null {
|
|
|
228
384
|
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
229
385
|
}
|
|
230
386
|
|
|
387
|
+
function normalizeText(value: unknown): string | null {
|
|
388
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
389
|
+
}
|
|
390
|
+
|
|
231
391
|
function normalizeRemoteProvider(value: unknown): OpenClawRemoteDeployProvider {
|
|
232
392
|
if (
|
|
233
393
|
value === 'hetzner'
|
|
@@ -245,6 +405,147 @@ function normalizeRemoteProvider(value: unknown): OpenClawRemoteDeployProvider {
|
|
|
245
405
|
return 'hetzner'
|
|
246
406
|
}
|
|
247
407
|
|
|
408
|
+
function normalizeUseCase(value: unknown): OpenClawUseCaseTemplate {
|
|
409
|
+
if (
|
|
410
|
+
value === 'local-dev'
|
|
411
|
+
|| value === 'single-vps'
|
|
412
|
+
|| value === 'private-tailnet'
|
|
413
|
+
|| value === 'browser-heavy'
|
|
414
|
+
|| value === 'team-control'
|
|
415
|
+
) {
|
|
416
|
+
return value
|
|
417
|
+
}
|
|
418
|
+
return 'single-vps'
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function normalizeExposurePreset(value: unknown, fallback?: OpenClawUseCaseTemplate): OpenClawExposurePreset {
|
|
422
|
+
if (
|
|
423
|
+
value === 'private-lan'
|
|
424
|
+
|| value === 'tailscale'
|
|
425
|
+
|| value === 'caddy'
|
|
426
|
+
|| value === 'nginx'
|
|
427
|
+
|| value === 'ssh-tunnel'
|
|
428
|
+
) {
|
|
429
|
+
return value
|
|
430
|
+
}
|
|
431
|
+
const useCase = fallback ? USE_CASE_META[fallback] : null
|
|
432
|
+
return useCase?.defaultExposure || 'private-lan'
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function sanitizeSshConfig(input?: Partial<OpenClawSshConfig> | null): OpenClawSshConfig | null {
|
|
436
|
+
const host = typeof input?.host === 'string' && input.host.trim() ? input.host.trim() : ''
|
|
437
|
+
if (!host) return null
|
|
438
|
+
const port = sanitizePort(input?.port, 22)
|
|
439
|
+
return {
|
|
440
|
+
host,
|
|
441
|
+
user: typeof input?.user === 'string' && input.user.trim() ? input.user.trim() : 'root',
|
|
442
|
+
port,
|
|
443
|
+
keyPath: typeof input?.keyPath === 'string' && input.keyPath.trim() ? input.keyPath.trim() : null,
|
|
444
|
+
targetDir: typeof input?.targetDir === 'string' && input.targetDir.trim() ? input.targetDir.trim() : '/opt/openclaw',
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function buildSshTarget(config: OpenClawSshConfig): string {
|
|
449
|
+
return `${config.user || 'root'}@${config.host}`
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function buildSshArgs(config: OpenClawSshConfig, forScp = false): string[] {
|
|
453
|
+
const args: string[] = ['-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=accept-new']
|
|
454
|
+
if (config.keyPath) args.push('-i', config.keyPath)
|
|
455
|
+
args.push(forScp ? '-P' : '-p', String(config.port || 22))
|
|
456
|
+
return args
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function materializeBundleFiles(bundle: OpenClawDeployBundle): Promise<{ dir: string; filePaths: string[] }> {
|
|
460
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'swarmclaw-openclaw-'))
|
|
461
|
+
const filePaths: string[] = []
|
|
462
|
+
for (const file of bundle.files) {
|
|
463
|
+
const filePath = path.join(dir, file.name)
|
|
464
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
465
|
+
await fs.writeFile(filePath, file.content, 'utf8')
|
|
466
|
+
filePaths.push(filePath)
|
|
467
|
+
}
|
|
468
|
+
return { dir, filePaths }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function updateRemoteRuntimeState(patch: Partial<RemoteRuntimeState>) {
|
|
472
|
+
Object.assign(getRuntimeState().remote, patch)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function startRemoteCommand(params: {
|
|
476
|
+
action: string
|
|
477
|
+
target: string
|
|
478
|
+
command: string
|
|
479
|
+
summary: string
|
|
480
|
+
backupPath?: string | null
|
|
481
|
+
}): Promise<OpenClawRemoteCommandResult> {
|
|
482
|
+
const result = await startManagedProcess({
|
|
483
|
+
command: params.command,
|
|
484
|
+
cwd: process.cwd(),
|
|
485
|
+
background: true,
|
|
486
|
+
timeoutMs: 30 * 60_000,
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
if (result.status === 'completed' && (result.exitCode ?? 0) === 0) {
|
|
490
|
+
updateRemoteRuntimeState({
|
|
491
|
+
processId: null,
|
|
492
|
+
action: params.action,
|
|
493
|
+
target: params.target,
|
|
494
|
+
startedAt: Date.now(),
|
|
495
|
+
lastError: null,
|
|
496
|
+
lastSummary: params.summary,
|
|
497
|
+
lastCommandPreview: params.command,
|
|
498
|
+
lastBackupPath: params.backupPath || null,
|
|
499
|
+
})
|
|
500
|
+
return {
|
|
501
|
+
ok: true,
|
|
502
|
+
started: false,
|
|
503
|
+
processId: null,
|
|
504
|
+
summary: params.summary,
|
|
505
|
+
commandPreview: params.command,
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (result.status !== 'running') {
|
|
510
|
+
const message = result.output || result.tail || params.summary
|
|
511
|
+
updateRemoteRuntimeState({
|
|
512
|
+
processId: null,
|
|
513
|
+
action: params.action,
|
|
514
|
+
target: params.target,
|
|
515
|
+
startedAt: null,
|
|
516
|
+
lastError: message,
|
|
517
|
+
lastSummary: params.summary,
|
|
518
|
+
lastCommandPreview: params.command,
|
|
519
|
+
lastBackupPath: params.backupPath || null,
|
|
520
|
+
})
|
|
521
|
+
return {
|
|
522
|
+
ok: false,
|
|
523
|
+
started: false,
|
|
524
|
+
processId: null,
|
|
525
|
+
summary: message,
|
|
526
|
+
commandPreview: params.command,
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
updateRemoteRuntimeState({
|
|
531
|
+
processId: result.processId,
|
|
532
|
+
action: params.action,
|
|
533
|
+
target: params.target,
|
|
534
|
+
startedAt: Date.now(),
|
|
535
|
+
lastError: null,
|
|
536
|
+
lastSummary: params.summary,
|
|
537
|
+
lastCommandPreview: params.command,
|
|
538
|
+
lastBackupPath: params.backupPath || null,
|
|
539
|
+
})
|
|
540
|
+
return {
|
|
541
|
+
ok: true,
|
|
542
|
+
started: true,
|
|
543
|
+
processId: result.processId,
|
|
544
|
+
summary: params.summary,
|
|
545
|
+
commandPreview: params.command,
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
248
549
|
function wait(ms: number): Promise<void> {
|
|
249
550
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
250
551
|
}
|
|
@@ -296,6 +597,38 @@ export function getOpenClawLocalDeployStatus(): OpenClawLocalDeployStatus {
|
|
|
296
597
|
return currentLocalStatus()
|
|
297
598
|
}
|
|
298
599
|
|
|
600
|
+
function currentRemoteStatus(): OpenClawRemoteDeployStatus {
|
|
601
|
+
const state = getRuntimeState()
|
|
602
|
+
const processId = state.remote.processId
|
|
603
|
+
const process = processId ? getManagedProcess(processId) : null
|
|
604
|
+
const active = !!process && process.status === 'running'
|
|
605
|
+
|
|
606
|
+
if (!active && processId && process && process.status !== 'running') {
|
|
607
|
+
state.remote.lastError = readTail(process.log || '') || state.remote.lastError
|
|
608
|
+
state.remote.processId = null
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
active,
|
|
613
|
+
processId: active ? processId : null,
|
|
614
|
+
pid: active ? (process?.pid ?? null) : null,
|
|
615
|
+
action: state.remote.action || null,
|
|
616
|
+
target: state.remote.target || null,
|
|
617
|
+
startedAt: state.remote.startedAt || null,
|
|
618
|
+
status: process?.status || 'idle',
|
|
619
|
+
exitCode: process?.exitCode ?? null,
|
|
620
|
+
tail: process ? readTail(process.log || '') : '',
|
|
621
|
+
lastError: active ? null : (state.remote.lastError || null),
|
|
622
|
+
lastSummary: state.remote.lastSummary || null,
|
|
623
|
+
lastCommandPreview: state.remote.lastCommandPreview || null,
|
|
624
|
+
lastBackupPath: state.remote.lastBackupPath || null,
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export function getOpenClawRemoteDeployStatus(): OpenClawRemoteDeployStatus {
|
|
629
|
+
return currentRemoteStatus()
|
|
630
|
+
}
|
|
631
|
+
|
|
299
632
|
export function generateOpenClawGatewayToken(): string {
|
|
300
633
|
return randomBytes(24).toString('base64url')
|
|
301
634
|
}
|
|
@@ -387,6 +720,17 @@ export function stopOpenClawLocalDeploy(): OpenClawLocalDeployStatus {
|
|
|
387
720
|
return currentLocalStatus()
|
|
388
721
|
}
|
|
389
722
|
|
|
723
|
+
export async function restartOpenClawLocalDeploy(input?: {
|
|
724
|
+
port?: number
|
|
725
|
+
token?: string | null
|
|
726
|
+
}): Promise<{ local: OpenClawLocalDeployStatus; token: string }> {
|
|
727
|
+
const current = currentLocalStatus()
|
|
728
|
+
return startOpenClawLocalDeploy({
|
|
729
|
+
port: input?.port ?? current.port,
|
|
730
|
+
token: input?.token ?? current.token,
|
|
731
|
+
})
|
|
732
|
+
}
|
|
733
|
+
|
|
390
734
|
function ensureSchemeAndPort(raw: string, scheme: 'http' | 'https', port: number): string {
|
|
391
735
|
const trimmed = raw.trim()
|
|
392
736
|
if (!trimmed) {
|
|
@@ -417,21 +761,36 @@ function indentBlock(value: string, spaces: number): string {
|
|
|
417
761
|
.join('\n')
|
|
418
762
|
}
|
|
419
763
|
|
|
420
|
-
|
|
764
|
+
interface DockerBundleOptions {
|
|
765
|
+
token: string
|
|
766
|
+
endpointHost: string
|
|
767
|
+
useCase: OpenClawUseCaseTemplate
|
|
768
|
+
exposure: OpenClawExposurePreset
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function resolveHostBindAddress(useCase: OpenClawUseCaseTemplate, exposure: OpenClawExposurePreset): string {
|
|
772
|
+
if (exposure === 'tailscale' || exposure === 'ssh-tunnel' || exposure === 'caddy' || exposure === 'nginx') {
|
|
773
|
+
return '127.0.0.1'
|
|
774
|
+
}
|
|
775
|
+
return USE_CASE_META[useCase].hostBind
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function buildDockerComposeFile(options: DockerBundleOptions): string {
|
|
421
779
|
return `services:
|
|
422
780
|
openclaw-gateway:
|
|
423
781
|
image: \${OPENCLAW_IMAGE:-openclaw:latest}
|
|
424
782
|
environment:
|
|
425
783
|
HOME: /home/node
|
|
426
784
|
TERM: xterm-256color
|
|
785
|
+
NODE_OPTIONS: \${OPENCLAW_NODE_OPTIONS:-}
|
|
427
786
|
OPENCLAW_GATEWAY_TOKEN: \${OPENCLAW_GATEWAY_TOKEN}
|
|
428
787
|
OPENCLAW_GATEWAY_BIND: \${OPENCLAW_GATEWAY_BIND:-lan}
|
|
429
788
|
volumes:
|
|
430
789
|
- \${OPENCLAW_CONFIG_DIR:-./.openclaw}:/home/node/.openclaw
|
|
431
790
|
- \${OPENCLAW_WORKSPACE_DIR:-./workspace}:/home/node/.openclaw/workspace
|
|
432
791
|
ports:
|
|
433
|
-
- "\${OPENCLAW_GATEWAY_PORT:-18789}:18789"
|
|
434
|
-
- "\${OPENCLAW_BRIDGE_PORT:-18790}:18790"
|
|
792
|
+
- "\${OPENCLAW_HOST_BIND:-${resolveHostBindAddress(options.useCase, options.exposure)}}:\${OPENCLAW_GATEWAY_PORT:-18789}:18789"
|
|
793
|
+
- "\${OPENCLAW_HOST_BIND:-${resolveHostBindAddress(options.useCase, options.exposure)}}:\${OPENCLAW_BRIDGE_PORT:-18790}:18790"
|
|
435
794
|
init: true
|
|
436
795
|
restart: unless-stopped
|
|
437
796
|
command:
|
|
@@ -464,18 +823,22 @@ function buildDockerComposeFile(): string {
|
|
|
464
823
|
`
|
|
465
824
|
}
|
|
466
825
|
|
|
467
|
-
function buildDockerEnvFile(
|
|
826
|
+
function buildDockerEnvFile(options: DockerBundleOptions): string {
|
|
468
827
|
return `OPENCLAW_IMAGE=openclaw:latest
|
|
469
|
-
OPENCLAW_GATEWAY_TOKEN=${token}
|
|
828
|
+
OPENCLAW_GATEWAY_TOKEN=${options.token}
|
|
470
829
|
OPENCLAW_GATEWAY_BIND=lan
|
|
830
|
+
OPENCLAW_HOST_BIND=${resolveHostBindAddress(options.useCase, options.exposure)}
|
|
471
831
|
OPENCLAW_GATEWAY_PORT=18789
|
|
472
832
|
OPENCLAW_BRIDGE_PORT=18790
|
|
473
833
|
OPENCLAW_CONFIG_DIR=./.openclaw
|
|
474
834
|
OPENCLAW_WORKSPACE_DIR=./workspace
|
|
835
|
+
OPENCLAW_USE_CASE=${options.useCase}
|
|
836
|
+
OPENCLAW_EXPOSURE=${options.exposure}
|
|
837
|
+
OPENCLAW_NODE_OPTIONS=${USE_CASE_META[options.useCase].nodeOptions || ''}
|
|
475
838
|
`
|
|
476
839
|
}
|
|
477
840
|
|
|
478
|
-
function buildDockerBootstrapScript(): string {
|
|
841
|
+
function buildDockerBootstrapScript(options: DockerBundleOptions): string {
|
|
479
842
|
return `#!/usr/bin/env bash
|
|
480
843
|
set -euo pipefail
|
|
481
844
|
|
|
@@ -483,7 +846,7 @@ APP_DIR="\${OPENCLAW_APP_DIR:-$HOME/openclaw}"
|
|
|
483
846
|
|
|
484
847
|
mkdir -p "$APP_DIR"
|
|
485
848
|
cd "$APP_DIR"
|
|
486
|
-
mkdir -p .openclaw workspace
|
|
849
|
+
mkdir -p .openclaw workspace backups
|
|
487
850
|
|
|
488
851
|
if ! command -v docker >/dev/null 2>&1; then
|
|
489
852
|
echo "Docker is required. On Ubuntu 24.04 you can install it with:"
|
|
@@ -493,13 +856,27 @@ fi
|
|
|
493
856
|
|
|
494
857
|
docker pull "\${OPENCLAW_IMAGE:-openclaw:latest}"
|
|
495
858
|
docker compose up -d
|
|
859
|
+
if [ -f docker-compose.proxy.yml ]; then
|
|
860
|
+
docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d
|
|
861
|
+
fi
|
|
496
862
|
docker compose ps
|
|
863
|
+
echo "Use case: ${options.useCase}"
|
|
864
|
+
echo "Exposure preset: ${options.exposure}"
|
|
497
865
|
`
|
|
498
866
|
}
|
|
499
867
|
|
|
500
|
-
function buildCloudInitFile(
|
|
501
|
-
const envFile = buildDockerEnvFile(
|
|
502
|
-
const composeFile = buildDockerComposeFile()
|
|
868
|
+
function buildCloudInitFile(options: DockerBundleOptions): string {
|
|
869
|
+
const envFile = buildDockerEnvFile(options)
|
|
870
|
+
const composeFile = buildDockerComposeFile(options)
|
|
871
|
+
const bootstrapFile = buildDockerBootstrapScript(options)
|
|
872
|
+
const extraFiles = buildExposureFiles(options)
|
|
873
|
+
.filter((file) => !['.env', 'docker-compose.yml', 'bootstrap.sh'].includes(file.name))
|
|
874
|
+
.map((file) => ` - path: /opt/openclaw/${file.name}
|
|
875
|
+
owner: root:root
|
|
876
|
+
permissions: "${file.name.endsWith('.sh') ? '0755' : '0644'}"
|
|
877
|
+
content: |
|
|
878
|
+
${indentBlock(file.content, 6)}`)
|
|
879
|
+
.join('\n')
|
|
503
880
|
return `#cloud-config
|
|
504
881
|
package_update: true
|
|
505
882
|
package_upgrade: true
|
|
@@ -519,15 +896,131 @@ ${indentBlock(envFile, 6)}
|
|
|
519
896
|
permissions: "0644"
|
|
520
897
|
content: |
|
|
521
898
|
${indentBlock(composeFile, 6)}
|
|
522
|
-
|
|
523
|
-
|
|
899
|
+
- path: /opt/openclaw/bootstrap.sh
|
|
900
|
+
owner: root:root
|
|
901
|
+
permissions: "0755"
|
|
902
|
+
content: |
|
|
903
|
+
${indentBlock(bootstrapFile, 6)}
|
|
904
|
+
${extraFiles ? `${extraFiles}
|
|
905
|
+
` : ''}runcmd:
|
|
906
|
+
- mkdir -p /opt/openclaw/.openclaw /opt/openclaw/workspace /opt/openclaw/backups
|
|
524
907
|
- systemctl enable --now docker
|
|
525
908
|
- bash -lc 'cd /opt/openclaw && docker pull "\${OPENCLAW_IMAGE:-openclaw:latest}"'
|
|
526
909
|
- bash -lc 'cd /opt/openclaw && docker compose up -d'
|
|
910
|
+
- bash -lc 'cd /opt/openclaw && if [ -f docker-compose.proxy.yml ]; then docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d; fi'
|
|
527
911
|
final_message: "OpenClaw gateway bootstrap complete. Run: sudo docker compose -f /opt/openclaw/docker-compose.yml ps"
|
|
528
912
|
`
|
|
529
913
|
}
|
|
530
914
|
|
|
915
|
+
function buildCaddyComposeFile(): string {
|
|
916
|
+
return `services:
|
|
917
|
+
caddy:
|
|
918
|
+
image: caddy:2
|
|
919
|
+
restart: unless-stopped
|
|
920
|
+
depends_on:
|
|
921
|
+
- openclaw-gateway
|
|
922
|
+
ports:
|
|
923
|
+
- "80:80"
|
|
924
|
+
- "443:443"
|
|
925
|
+
volumes:
|
|
926
|
+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
927
|
+
- caddy_data:/data
|
|
928
|
+
- caddy_config:/config
|
|
929
|
+
|
|
930
|
+
volumes:
|
|
931
|
+
caddy_data:
|
|
932
|
+
caddy_config:
|
|
933
|
+
`
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function buildCaddyfile(endpointHost: string): string {
|
|
937
|
+
return `${endpointHost} {
|
|
938
|
+
encode gzip
|
|
939
|
+
reverse_proxy openclaw-gateway:18789
|
|
940
|
+
}
|
|
941
|
+
`
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function buildNginxComposeFile(): string {
|
|
945
|
+
return `services:
|
|
946
|
+
nginx:
|
|
947
|
+
image: nginx:1.27-alpine
|
|
948
|
+
restart: unless-stopped
|
|
949
|
+
depends_on:
|
|
950
|
+
- openclaw-gateway
|
|
951
|
+
ports:
|
|
952
|
+
- "80:80"
|
|
953
|
+
volumes:
|
|
954
|
+
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
|
955
|
+
`
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function buildNginxConfig(endpointHost: string): string {
|
|
959
|
+
return `server {
|
|
960
|
+
listen 80;
|
|
961
|
+
server_name ${endpointHost};
|
|
962
|
+
|
|
963
|
+
location / {
|
|
964
|
+
proxy_pass http://openclaw-gateway:18789;
|
|
965
|
+
proxy_http_version 1.1;
|
|
966
|
+
proxy_set_header Host $host;
|
|
967
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
968
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
969
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
970
|
+
proxy_set_header Connection "upgrade";
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
`
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function buildTailscaleServeScript(): string {
|
|
977
|
+
return `#!/usr/bin/env bash
|
|
978
|
+
set -euo pipefail
|
|
979
|
+
|
|
980
|
+
PORT="\${OPENCLAW_GATEWAY_PORT:-18789}"
|
|
981
|
+
if ! command -v tailscale >/dev/null 2>&1; then
|
|
982
|
+
echo "Install Tailscale first: https://tailscale.com/download"
|
|
983
|
+
exit 1
|
|
984
|
+
fi
|
|
985
|
+
|
|
986
|
+
sudo tailscale serve --bg --set-path=/ http://127.0.0.1:$PORT
|
|
987
|
+
tailscale status
|
|
988
|
+
`
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function buildSshTunnelGuide(endpointHost: string): string {
|
|
992
|
+
return `Use an SSH tunnel instead of opening the gateway publicly.
|
|
993
|
+
|
|
994
|
+
Example:
|
|
995
|
+
ssh -N -L 18789:127.0.0.1:18789 user@${endpointHost}
|
|
996
|
+
|
|
997
|
+
Then point SwarmClaw at:
|
|
998
|
+
http://127.0.0.1:18789/v1
|
|
999
|
+
`
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function buildExposureFiles(options: DockerBundleOptions): OpenClawDeployBundleFile[] {
|
|
1003
|
+
if (options.exposure === 'caddy') {
|
|
1004
|
+
return [
|
|
1005
|
+
{ name: 'docker-compose.proxy.yml', language: 'yaml', content: buildCaddyComposeFile() },
|
|
1006
|
+
{ name: 'Caddyfile', language: 'text', content: buildCaddyfile(options.endpointHost) },
|
|
1007
|
+
]
|
|
1008
|
+
}
|
|
1009
|
+
if (options.exposure === 'nginx') {
|
|
1010
|
+
return [
|
|
1011
|
+
{ name: 'docker-compose.proxy.yml', language: 'yaml', content: buildNginxComposeFile() },
|
|
1012
|
+
{ name: 'nginx.conf', language: 'text', content: buildNginxConfig(options.endpointHost) },
|
|
1013
|
+
]
|
|
1014
|
+
}
|
|
1015
|
+
if (options.exposure === 'tailscale') {
|
|
1016
|
+
return [{ name: 'tailscale-serve.sh', language: 'bash', content: buildTailscaleServeScript() }]
|
|
1017
|
+
}
|
|
1018
|
+
if (options.exposure === 'ssh-tunnel') {
|
|
1019
|
+
return [{ name: 'ssh-tunnel.txt', language: 'text', content: buildSshTunnelGuide(options.endpointHost) }]
|
|
1020
|
+
}
|
|
1021
|
+
return []
|
|
1022
|
+
}
|
|
1023
|
+
|
|
531
1024
|
function buildRenderManifest(): string {
|
|
532
1025
|
return `services:
|
|
533
1026
|
- type: web
|
|
@@ -610,12 +1103,23 @@ function buildRailwayConfig(): string {
|
|
|
610
1103
|
function buildDockerRunbook(
|
|
611
1104
|
providerMeta: RemoteProviderMeta,
|
|
612
1105
|
endpoint: string,
|
|
1106
|
+
useCase: OpenClawUseCaseTemplate,
|
|
1107
|
+
exposure: OpenClawExposurePreset,
|
|
613
1108
|
): string[] {
|
|
614
1109
|
const endpointHost = deriveRemoteDeploymentName(endpoint)
|
|
615
1110
|
return [
|
|
616
1111
|
`Provision a small Ubuntu 24.04 server on ${providerMeta.label}. ${providerMeta.bootstrapHint}`,
|
|
1112
|
+
`Use case preset: ${USE_CASE_META[useCase].label}. Exposure preset: ${EXPOSURE_META[exposure].label}.`,
|
|
617
1113
|
'Let first boot finish, then confirm the service with: sudo docker compose -f /opt/openclaw/docker-compose.yml ps',
|
|
618
|
-
|
|
1114
|
+
exposure === 'tailscale'
|
|
1115
|
+
? 'Run tailscale-serve.sh after the host joins your tailnet so OpenClaw stays private.'
|
|
1116
|
+
: exposure === 'caddy'
|
|
1117
|
+
? 'Set your DNS name to this host and start the bundled Caddy proxy for HTTPS termination.'
|
|
1118
|
+
: exposure === 'nginx'
|
|
1119
|
+
? 'Start the bundled Nginx proxy or bring your own TLS terminator in front of the gateway.'
|
|
1120
|
+
: exposure === 'ssh-tunnel'
|
|
1121
|
+
? 'Do not expose the gateway publicly; use the generated SSH tunnel guide instead.'
|
|
1122
|
+
: `Point a DNS name, reverse proxy, or private network hostname at ${endpointHost} and keep the generated token private.`,
|
|
619
1123
|
'Use the generated endpoint and token in SwarmClaw to save the gateway profile.',
|
|
620
1124
|
]
|
|
621
1125
|
}
|
|
@@ -627,6 +1131,8 @@ export function buildOpenClawDeployBundle(input?: {
|
|
|
627
1131
|
scheme?: 'http' | 'https'
|
|
628
1132
|
port?: number
|
|
629
1133
|
provider?: OpenClawRemoteDeployProvider
|
|
1134
|
+
useCase?: OpenClawUseCaseTemplate
|
|
1135
|
+
exposure?: OpenClawExposurePreset
|
|
630
1136
|
}): OpenClawDeployBundle {
|
|
631
1137
|
const template = input?.template || 'docker'
|
|
632
1138
|
const token = normalizeToken(input?.token) || generateOpenClawGatewayToken()
|
|
@@ -637,12 +1143,22 @@ export function buildOpenClawDeployBundle(input?: {
|
|
|
637
1143
|
const wsUrl = deriveOpenClawWsUrl(endpoint)
|
|
638
1144
|
const provider = normalizeRemoteProvider(input?.provider)
|
|
639
1145
|
const providerMeta = REMOTE_PROVIDER_META[provider]
|
|
1146
|
+
const useCase = normalizeUseCase(input?.useCase)
|
|
1147
|
+
const exposure = normalizeExposurePreset(input?.exposure, useCase)
|
|
1148
|
+
const bundleOptions: DockerBundleOptions = {
|
|
1149
|
+
token,
|
|
1150
|
+
endpointHost: deriveRemoteDeploymentName(endpoint),
|
|
1151
|
+
useCase,
|
|
1152
|
+
exposure,
|
|
1153
|
+
}
|
|
640
1154
|
|
|
641
1155
|
if (template === 'render') {
|
|
642
1156
|
return {
|
|
643
1157
|
template,
|
|
644
1158
|
provider: 'generic',
|
|
645
1159
|
providerLabel: 'Render',
|
|
1160
|
+
useCase,
|
|
1161
|
+
exposure,
|
|
646
1162
|
title: 'Render OpenClaw Service',
|
|
647
1163
|
summary: 'Deploy the official OpenClaw repo as a Docker web service on Render, then point SwarmClaw at the public HTTPS URL.',
|
|
648
1164
|
endpoint,
|
|
@@ -665,6 +1181,8 @@ export function buildOpenClawDeployBundle(input?: {
|
|
|
665
1181
|
template,
|
|
666
1182
|
provider: 'generic',
|
|
667
1183
|
providerLabel: 'Fly.io',
|
|
1184
|
+
useCase,
|
|
1185
|
+
exposure,
|
|
668
1186
|
title: 'Fly.io OpenClaw App',
|
|
669
1187
|
summary: 'Deploy the official OpenClaw repo on Fly.io for an always-on remote gateway with a persistent volume and HTTPS out of the box.',
|
|
670
1188
|
endpoint,
|
|
@@ -687,6 +1205,8 @@ export function buildOpenClawDeployBundle(input?: {
|
|
|
687
1205
|
template,
|
|
688
1206
|
provider: 'generic',
|
|
689
1207
|
providerLabel: 'Railway',
|
|
1208
|
+
useCase,
|
|
1209
|
+
exposure,
|
|
690
1210
|
title: 'Railway OpenClaw Service',
|
|
691
1211
|
summary: 'Deploy the official OpenClaw repo on Railway using its Dockerfile, then attach a volume and set the generated gateway token.',
|
|
692
1212
|
endpoint,
|
|
@@ -708,17 +1228,157 @@ export function buildOpenClawDeployBundle(input?: {
|
|
|
708
1228
|
template: 'docker',
|
|
709
1229
|
provider,
|
|
710
1230
|
providerLabel: providerMeta.shortLabel,
|
|
1231
|
+
useCase,
|
|
1232
|
+
exposure,
|
|
711
1233
|
title: `${providerMeta.shortLabel} OpenClaw Smart Deploy`,
|
|
712
|
-
summary: `${providerMeta.summary} This bundle only uses the official OpenClaw Docker image and gives you both manual Docker files and a cloud-init quickstart.`,
|
|
1234
|
+
summary: `${providerMeta.summary} ${USE_CASE_META[useCase].detail} This bundle only uses the official OpenClaw Docker image and gives you both manual Docker files and a cloud-init quickstart.`,
|
|
713
1235
|
endpoint,
|
|
714
1236
|
wsUrl,
|
|
715
1237
|
token,
|
|
716
|
-
runbook: buildDockerRunbook(providerMeta, endpoint),
|
|
1238
|
+
runbook: buildDockerRunbook(providerMeta, endpoint, useCase, exposure),
|
|
717
1239
|
files: [
|
|
718
|
-
{ name: 'cloud-init.yaml', language: 'yaml', content: buildCloudInitFile(
|
|
719
|
-
{ name: '.env', language: 'env', content: buildDockerEnvFile(
|
|
720
|
-
{ name: 'docker-compose.yml', language: 'yaml', content: buildDockerComposeFile() },
|
|
721
|
-
{ name: 'bootstrap.sh', language: 'bash', content: buildDockerBootstrapScript() },
|
|
1240
|
+
{ name: 'cloud-init.yaml', language: 'yaml', content: buildCloudInitFile(bundleOptions) },
|
|
1241
|
+
{ name: '.env', language: 'env', content: buildDockerEnvFile(bundleOptions) },
|
|
1242
|
+
{ name: 'docker-compose.yml', language: 'yaml', content: buildDockerComposeFile(bundleOptions) },
|
|
1243
|
+
{ name: 'bootstrap.sh', language: 'bash', content: buildDockerBootstrapScript(bundleOptions) },
|
|
1244
|
+
...buildExposureFiles(bundleOptions),
|
|
722
1245
|
],
|
|
723
1246
|
}
|
|
724
1247
|
}
|
|
1248
|
+
|
|
1249
|
+
function buildSshInvocation(config: OpenClawSshConfig, remoteCommand: string): string {
|
|
1250
|
+
return ['ssh', ...buildSshArgs(config), buildSshTarget(config), remoteCommand]
|
|
1251
|
+
.map(shellEscape)
|
|
1252
|
+
.join(' ')
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function buildScpInvocation(config: OpenClawSshConfig, filePaths: string[]): string {
|
|
1256
|
+
const destination = `${buildSshTarget(config)}:${config.targetDir || '/opt/openclaw'}/`
|
|
1257
|
+
return ['scp', ...buildSshArgs(config, true), ...filePaths, destination]
|
|
1258
|
+
.map(shellEscape)
|
|
1259
|
+
.join(' ')
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
export async function verifyOpenClawDeployment(input?: {
|
|
1263
|
+
endpoint?: string | null
|
|
1264
|
+
credentialId?: string | null
|
|
1265
|
+
token?: string | null
|
|
1266
|
+
model?: string | null
|
|
1267
|
+
timeoutMs?: number
|
|
1268
|
+
}): Promise<OpenClawHealthResult> {
|
|
1269
|
+
return probeOpenClawHealth({
|
|
1270
|
+
endpoint: input?.endpoint || null,
|
|
1271
|
+
credentialId: input?.credentialId || null,
|
|
1272
|
+
token: input?.token || null,
|
|
1273
|
+
model: input?.model || null,
|
|
1274
|
+
timeoutMs: input?.timeoutMs,
|
|
1275
|
+
})
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
export async function deployOpenClawBundleOverSsh(input?: {
|
|
1279
|
+
template?: OpenClawRemoteDeployTemplate
|
|
1280
|
+
target?: string | null
|
|
1281
|
+
token?: string | null
|
|
1282
|
+
scheme?: 'http' | 'https'
|
|
1283
|
+
port?: number
|
|
1284
|
+
provider?: OpenClawRemoteDeployProvider
|
|
1285
|
+
useCase?: OpenClawUseCaseTemplate
|
|
1286
|
+
exposure?: OpenClawExposurePreset
|
|
1287
|
+
ssh?: Partial<OpenClawSshConfig> | null
|
|
1288
|
+
}): Promise<OpenClawRemoteCommandResult> {
|
|
1289
|
+
const sshConfig = sanitizeSshConfig(input?.ssh)
|
|
1290
|
+
if (!sshConfig) throw new Error('SSH host is required for remote deploy.')
|
|
1291
|
+
|
|
1292
|
+
const bundle = buildOpenClawDeployBundle({
|
|
1293
|
+
template: input?.template,
|
|
1294
|
+
target: input?.target,
|
|
1295
|
+
token: input?.token,
|
|
1296
|
+
scheme: input?.scheme,
|
|
1297
|
+
port: input?.port,
|
|
1298
|
+
provider: input?.provider,
|
|
1299
|
+
useCase: input?.useCase,
|
|
1300
|
+
exposure: input?.exposure,
|
|
1301
|
+
})
|
|
1302
|
+
const materialized = await materializeBundleFiles(bundle)
|
|
1303
|
+
const remoteDir = sshConfig.targetDir || '/opt/openclaw'
|
|
1304
|
+
const mkdirCommand = buildSshInvocation(sshConfig, `mkdir -p ${shellEscape(remoteDir)}`)
|
|
1305
|
+
const scpCommand = buildScpInvocation(sshConfig, materialized.filePaths)
|
|
1306
|
+
const bootstrapCommand = buildSshInvocation(
|
|
1307
|
+
sshConfig,
|
|
1308
|
+
`cd ${shellEscape(remoteDir)} && chmod +x bootstrap.sh && OPENCLAW_APP_DIR=${shellEscape(remoteDir)} bash ./bootstrap.sh`,
|
|
1309
|
+
)
|
|
1310
|
+
const command = `${mkdirCommand} && ${scpCommand} && ${bootstrapCommand}`
|
|
1311
|
+
const result = await startRemoteCommand({
|
|
1312
|
+
action: 'ssh-deploy',
|
|
1313
|
+
target: sshConfig.host,
|
|
1314
|
+
command,
|
|
1315
|
+
summary: `Deploying OpenClaw to ${sshConfig.host} over SSH.`,
|
|
1316
|
+
})
|
|
1317
|
+
return {
|
|
1318
|
+
...result,
|
|
1319
|
+
token: bundle.token,
|
|
1320
|
+
bundle,
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
export const deployOpenClawOverSsh = deployOpenClawBundleOverSsh
|
|
1325
|
+
|
|
1326
|
+
export async function runOpenClawRemoteLifecycleAction(input?: {
|
|
1327
|
+
action: 'start' | 'stop' | 'restart' | 'upgrade' | 'backup' | 'restore' | 'rotate-token'
|
|
1328
|
+
ssh?: Partial<OpenClawSshConfig> | null
|
|
1329
|
+
image?: string | null
|
|
1330
|
+
token?: string | null
|
|
1331
|
+
backupPath?: string | null
|
|
1332
|
+
}): Promise<OpenClawRemoteCommandResult> {
|
|
1333
|
+
const sshConfig = sanitizeSshConfig(input?.ssh)
|
|
1334
|
+
if (!sshConfig) throw new Error('SSH host is required for remote lifecycle actions.')
|
|
1335
|
+
const remoteDir = sshConfig.targetDir || '/opt/openclaw'
|
|
1336
|
+
const image = normalizeText(input?.image) || 'openclaw:latest'
|
|
1337
|
+
const action = input?.action || 'restart'
|
|
1338
|
+
let remoteCommand = ''
|
|
1339
|
+
let summary = ''
|
|
1340
|
+
let rotatedToken: string | undefined
|
|
1341
|
+
let backupPath: string | null = null
|
|
1342
|
+
|
|
1343
|
+
if (action === 'start') {
|
|
1344
|
+
remoteCommand = `cd ${shellEscape(remoteDir)} && docker compose up -d`
|
|
1345
|
+
summary = `Starting OpenClaw on ${sshConfig.host}.`
|
|
1346
|
+
} else if (action === 'stop') {
|
|
1347
|
+
remoteCommand = `cd ${shellEscape(remoteDir)} && docker compose down`
|
|
1348
|
+
summary = `Stopping OpenClaw on ${sshConfig.host}.`
|
|
1349
|
+
} else if (action === 'restart') {
|
|
1350
|
+
remoteCommand = `cd ${shellEscape(remoteDir)} && docker compose restart`
|
|
1351
|
+
summary = `Restarting OpenClaw on ${sshConfig.host}.`
|
|
1352
|
+
} else if (action === 'upgrade') {
|
|
1353
|
+
remoteCommand = `cd ${shellEscape(remoteDir)} && docker pull ${shellEscape(image)} && docker compose up -d`
|
|
1354
|
+
summary = `Pulling ${image} and recreating the OpenClaw stack on ${sshConfig.host}.`
|
|
1355
|
+
} else if (action === 'backup') {
|
|
1356
|
+
backupPath = path.posix.join(remoteDir, 'backups', `openclaw-backup-${Date.now()}.tgz`)
|
|
1357
|
+
remoteCommand = `cd ${shellEscape(remoteDir)} && mkdir -p backups && tar -czf ${shellEscape(backupPath)} .env docker-compose.yml .openclaw workspace && printf '%s\\n' ${shellEscape(backupPath)}`
|
|
1358
|
+
summary = `Creating an OpenClaw backup on ${sshConfig.host}.`
|
|
1359
|
+
} else if (action === 'restore') {
|
|
1360
|
+
backupPath = normalizeText(input?.backupPath) || null
|
|
1361
|
+
if (!backupPath) throw new Error('A remote backup path is required for restore.')
|
|
1362
|
+
remoteCommand = `cd ${shellEscape(remoteDir)} && tar -xzf ${shellEscape(backupPath)} -C ${shellEscape(remoteDir)} && docker compose up -d`
|
|
1363
|
+
summary = `Restoring OpenClaw from ${backupPath} on ${sshConfig.host}.`
|
|
1364
|
+
} else {
|
|
1365
|
+
rotatedToken = normalizeToken(input?.token) || generateOpenClawGatewayToken()
|
|
1366
|
+
remoteCommand = `cd ${shellEscape(remoteDir)} && sed -i.bak -e ${shellEscape(`s/^OPENCLAW_GATEWAY_TOKEN=.*/OPENCLAW_GATEWAY_TOKEN=${rotatedToken}/`)} .env && docker compose up -d && printf '%s\\n' ${shellEscape(rotatedToken)}`
|
|
1367
|
+
summary = `Rotating the OpenClaw gateway token on ${sshConfig.host}.`
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const command = buildSshInvocation(sshConfig, remoteCommand)
|
|
1371
|
+
const result = await startRemoteCommand({
|
|
1372
|
+
action,
|
|
1373
|
+
target: sshConfig.host,
|
|
1374
|
+
command,
|
|
1375
|
+
summary,
|
|
1376
|
+
backupPath,
|
|
1377
|
+
})
|
|
1378
|
+
return {
|
|
1379
|
+
...result,
|
|
1380
|
+
token: rotatedToken,
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
export const runOpenClawRemoteLifecycle = runOpenClawRemoteLifecycleAction
|