@swarmclawai/swarmclaw 0.7.6 → 0.7.7

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/src/cli/index.js CHANGED
@@ -319,10 +319,50 @@ const COMMAND_GROUPS = [
319
319
  expectsJsonBody: true,
320
320
  defaultBody: { action: 'stop-local' },
321
321
  }),
322
+ cmd('deploy-local-restart', 'POST', '/openclaw/deploy', 'Restart the managed local OpenClaw deployment (use --data JSON for port/token overrides)', {
323
+ expectsJsonBody: true,
324
+ defaultBody: { action: 'restart-local' },
325
+ }),
322
326
  cmd('deploy-bundle', 'POST', '/openclaw/deploy', 'Generate an OpenClaw remote deployment bundle (use --data JSON for template/target/token)', {
323
327
  expectsJsonBody: true,
324
328
  defaultBody: { action: 'bundle' },
325
329
  }),
330
+ cmd('deploy-ssh', 'POST', '/openclaw/deploy', 'Push the official-image OpenClaw bundle to a remote host over SSH (use --data JSON for target/ssh/provider)', {
331
+ expectsJsonBody: true,
332
+ defaultBody: { action: 'ssh-deploy' },
333
+ }),
334
+ cmd('deploy-verify', 'POST', '/openclaw/deploy', 'Verify an OpenClaw endpoint/token pair (use --data JSON for endpoint/token)', {
335
+ expectsJsonBody: true,
336
+ defaultBody: { action: 'verify' },
337
+ }),
338
+ cmd('remote-start', 'POST', '/openclaw/deploy', 'Start a remote SSH-managed OpenClaw stack', {
339
+ expectsJsonBody: true,
340
+ defaultBody: { action: 'remote-start' },
341
+ }),
342
+ cmd('remote-stop', 'POST', '/openclaw/deploy', 'Stop a remote SSH-managed OpenClaw stack', {
343
+ expectsJsonBody: true,
344
+ defaultBody: { action: 'remote-stop' },
345
+ }),
346
+ cmd('remote-restart', 'POST', '/openclaw/deploy', 'Restart a remote SSH-managed OpenClaw stack', {
347
+ expectsJsonBody: true,
348
+ defaultBody: { action: 'remote-restart' },
349
+ }),
350
+ cmd('remote-upgrade', 'POST', '/openclaw/deploy', 'Upgrade a remote SSH-managed OpenClaw stack', {
351
+ expectsJsonBody: true,
352
+ defaultBody: { action: 'remote-upgrade' },
353
+ }),
354
+ cmd('remote-backup', 'POST', '/openclaw/deploy', 'Create a remote backup on an SSH-managed OpenClaw host', {
355
+ expectsJsonBody: true,
356
+ defaultBody: { action: 'remote-backup' },
357
+ }),
358
+ cmd('remote-restore', 'POST', '/openclaw/deploy', 'Restore a remote backup on an SSH-managed OpenClaw host', {
359
+ expectsJsonBody: true,
360
+ defaultBody: { action: 'remote-restore' },
361
+ }),
362
+ cmd('remote-rotate-token', 'POST', '/openclaw/deploy', 'Rotate the gateway token on an SSH-managed OpenClaw host', {
363
+ expectsJsonBody: true,
364
+ defaultBody: { action: 'remote-rotate-token' },
365
+ }),
326
366
  cmd('directory', 'GET', '/openclaw/directory', 'List directory entries from running OpenClaw connectors'),
327
367
  cmd('gateway-status', 'GET', '/openclaw/gateway', 'Check OpenClaw gateway connection status'),
328
368
  cmd('gateway', 'POST', '/openclaw/gateway', 'Call OpenClaw gateway RPC/control action', { expectsJsonBody: true }),
@@ -197,6 +197,74 @@ test('openclaw deploy bundle command merges action with provided JSON body', asy
197
197
  assert.equal(stderr.toString(), '')
198
198
  })
199
199
 
200
+ test('openclaw deploy ssh command merges action with provided JSON body', async () => {
201
+ const stdout = makeWritable()
202
+ const stderr = makeWritable()
203
+ const calls = []
204
+
205
+ const fetchImpl = async (url, init) => {
206
+ calls.push({ url: String(url), init })
207
+ return jsonResponse({ ok: true, processId: 'remote-1' })
208
+ }
209
+
210
+ const exitCode = await runCli(
211
+ ['openclaw', 'deploy-ssh', '--data', '{"target":"openclaw.example.com","ssh":{"host":"1.2.3.4"}}', '--json'],
212
+ {
213
+ fetchImpl,
214
+ stdout,
215
+ stderr,
216
+ env: {},
217
+ cwd: process.cwd(),
218
+ }
219
+ )
220
+
221
+ assert.equal(exitCode, 0)
222
+ assert.equal(calls.length, 1)
223
+ assert.match(calls[0].url, /\/api\/openclaw\/deploy$/)
224
+ assert.equal(calls[0].init.method, 'POST')
225
+ assert.deepEqual(JSON.parse(String(calls[0].init.body)), {
226
+ action: 'ssh-deploy',
227
+ target: 'openclaw.example.com',
228
+ ssh: { host: '1.2.3.4' },
229
+ })
230
+ assert.equal(stdout.toString().trim(), '{"ok":true,"processId":"remote-1"}')
231
+ assert.equal(stderr.toString(), '')
232
+ })
233
+
234
+ test('openclaw remote restore command merges action with provided JSON body', async () => {
235
+ const stdout = makeWritable()
236
+ const stderr = makeWritable()
237
+ const calls = []
238
+
239
+ const fetchImpl = async (url, init) => {
240
+ calls.push({ url: String(url), init })
241
+ return jsonResponse({ ok: true, remote: { status: 'running' } })
242
+ }
243
+
244
+ const exitCode = await runCli(
245
+ ['openclaw', 'remote-restore', '--data', '{"backupPath":"/opt/openclaw/backups/latest.tgz","ssh":{"host":"1.2.3.4"}}', '--json'],
246
+ {
247
+ fetchImpl,
248
+ stdout,
249
+ stderr,
250
+ env: {},
251
+ cwd: process.cwd(),
252
+ }
253
+ )
254
+
255
+ assert.equal(exitCode, 0)
256
+ assert.equal(calls.length, 1)
257
+ assert.match(calls[0].url, /\/api\/openclaw\/deploy$/)
258
+ assert.equal(calls[0].init.method, 'POST')
259
+ assert.deepEqual(JSON.parse(String(calls[0].init.body)), {
260
+ action: 'remote-restore',
261
+ backupPath: '/opt/openclaw/backups/latest.tgz',
262
+ ssh: { host: '1.2.3.4' },
263
+ })
264
+ assert.equal(stdout.toString().trim(), '{"ok":true,"remote":{"status":"running"}}')
265
+ assert.equal(stderr.toString(), '')
266
+ })
267
+
200
268
  test('runCli falls back to platform-api-key.txt when env key is missing', async () => {
201
269
  const stdout = makeWritable()
202
270
  const stderr = makeWritable()
package/src/cli/spec.js CHANGED
@@ -224,12 +224,72 @@ const COMMAND_GROUPS = {
224
224
  path: '/openclaw/deploy',
225
225
  staticBody: { action: 'stop-local' },
226
226
  },
227
+ 'deploy-local-restart': {
228
+ description: 'Restart the managed local OpenClaw deployment (use --data JSON for port/token overrides)',
229
+ method: 'POST',
230
+ path: '/openclaw/deploy',
231
+ staticBody: { action: 'restart-local' },
232
+ },
227
233
  'deploy-bundle': {
228
234
  description: 'Generate an OpenClaw remote deployment bundle (use --data JSON for template/target/token)',
229
235
  method: 'POST',
230
236
  path: '/openclaw/deploy',
231
237
  staticBody: { action: 'bundle' },
232
238
  },
239
+ 'deploy-ssh': {
240
+ description: 'Push the official-image OpenClaw bundle to a remote host over SSH (use --data JSON for target/ssh/provider)',
241
+ method: 'POST',
242
+ path: '/openclaw/deploy',
243
+ staticBody: { action: 'ssh-deploy' },
244
+ },
245
+ 'deploy-verify': {
246
+ description: 'Verify an OpenClaw endpoint/token pair (use --data JSON for endpoint/token)',
247
+ method: 'POST',
248
+ path: '/openclaw/deploy',
249
+ staticBody: { action: 'verify' },
250
+ },
251
+ 'remote-start': {
252
+ description: 'Start a remote SSH-managed OpenClaw stack',
253
+ method: 'POST',
254
+ path: '/openclaw/deploy',
255
+ staticBody: { action: 'remote-start' },
256
+ },
257
+ 'remote-stop': {
258
+ description: 'Stop a remote SSH-managed OpenClaw stack',
259
+ method: 'POST',
260
+ path: '/openclaw/deploy',
261
+ staticBody: { action: 'remote-stop' },
262
+ },
263
+ 'remote-restart': {
264
+ description: 'Restart a remote SSH-managed OpenClaw stack',
265
+ method: 'POST',
266
+ path: '/openclaw/deploy',
267
+ staticBody: { action: 'remote-restart' },
268
+ },
269
+ 'remote-upgrade': {
270
+ description: 'Upgrade a remote SSH-managed OpenClaw stack',
271
+ method: 'POST',
272
+ path: '/openclaw/deploy',
273
+ staticBody: { action: 'remote-upgrade' },
274
+ },
275
+ 'remote-backup': {
276
+ description: 'Create a remote backup on an SSH-managed OpenClaw host',
277
+ method: 'POST',
278
+ path: '/openclaw/deploy',
279
+ staticBody: { action: 'remote-backup' },
280
+ },
281
+ 'remote-restore': {
282
+ description: 'Restore a remote backup on an SSH-managed OpenClaw host',
283
+ method: 'POST',
284
+ path: '/openclaw/deploy',
285
+ staticBody: { action: 'remote-restore' },
286
+ },
287
+ 'remote-rotate-token': {
288
+ description: 'Rotate the gateway token on an SSH-managed OpenClaw host',
289
+ method: 'POST',
290
+ path: '/openclaw/deploy',
291
+ staticBody: { action: 'remote-rotate-token' },
292
+ },
233
293
  directory: { description: 'List directory entries from running OpenClaw connectors', method: 'GET', path: '/openclaw/directory' },
234
294
  'gateway-status': { description: 'Check OpenClaw gateway connection status', method: 'GET', path: '/openclaw/gateway' },
235
295
  gateway: { description: 'Call OpenClaw gateway RPC/control action', method: 'POST', path: '/openclaw/gateway' },
@@ -72,6 +72,24 @@ function parseIdentityList(value: string): string[] {
72
72
  })
73
73
  }
74
74
 
75
+ function formatGatewayTagList(value: string[] | null | undefined): string {
76
+ return Array.isArray(value) ? value.join(', ') : ''
77
+ }
78
+
79
+ function parseGatewayTagList(value: string): string[] {
80
+ const seen = new Set<string>()
81
+ return value
82
+ .split(/[,\n]/)
83
+ .map((entry) => entry.trim())
84
+ .filter((entry) => {
85
+ if (!entry) return false
86
+ const key = entry.toLowerCase()
87
+ if (seen.has(key)) return false
88
+ seen.add(key)
89
+ return true
90
+ })
91
+ }
92
+
75
93
  export function AgentSheet() {
76
94
  const open = useAppStore((s) => s.agentSheetOpen)
77
95
  const setOpen = useAppStore((s) => s.setAgentSheetOpen)
@@ -113,6 +131,8 @@ export function AgentSheet() {
113
131
  const [credentialId, setCredentialId] = useState<string | null>(null)
114
132
  const [apiEndpoint, setApiEndpoint] = useState<string | null>(null)
115
133
  const [gatewayProfileId, setGatewayProfileId] = useState<string | null>(null)
134
+ const [preferredGatewayTagsText, setPreferredGatewayTagsText] = useState('')
135
+ const [preferredGatewayUseCase, setPreferredGatewayUseCase] = useState('')
116
136
  const [routingStrategy, setRoutingStrategy] = useState<AgentRoutingStrategy>('single')
117
137
  const [routingTargets, setRoutingTargets] = useState<AgentRoutingTarget[]>([])
118
138
  const [platformAssignScope, setPlatformAssignScope] = useState<'self' | 'all'>('self')
@@ -220,6 +240,8 @@ export function AgentSheet() {
220
240
  setCredentialId(editing.credentialId || null)
221
241
  setApiEndpoint(editing.apiEndpoint || null)
222
242
  setGatewayProfileId(editing.gatewayProfileId || null)
243
+ setPreferredGatewayTagsText(formatGatewayTagList(editing.preferredGatewayTags))
244
+ setPreferredGatewayUseCase(editing.preferredGatewayUseCase || '')
223
245
  setRoutingStrategy(editing.routingStrategy || 'single')
224
246
  setRoutingTargets(editing.routingTargets || [])
225
247
  setPlatformAssignScope(editing.platformAssignScope || 'self')
@@ -287,6 +309,8 @@ export function AgentSheet() {
287
309
  setCredentialId(null)
288
310
  setApiEndpoint(null)
289
311
  setGatewayProfileId(null)
312
+ setPreferredGatewayTagsText('')
313
+ setPreferredGatewayUseCase('')
290
314
  setRoutingStrategy('single')
291
315
  setRoutingTargets([])
292
316
  setPlatformAssignScope('self')
@@ -422,6 +446,8 @@ export function AgentSheet() {
422
446
  fallbackCredentialIds,
423
447
  apiEndpoint,
424
448
  gatewayProfileId,
449
+ preferredGatewayTags: parseGatewayTagList(preferredGatewayTagsText),
450
+ preferredGatewayUseCase: preferredGatewayUseCase || null,
425
451
  priority: routingTargets.length + 1,
426
452
  }
427
453
  setRoutingTargets((current) => [...current, nextTarget])
@@ -463,9 +489,13 @@ export function AgentSheet() {
463
489
  credentialId,
464
490
  apiEndpoint: normalizedEndpoint,
465
491
  gatewayProfileId,
492
+ preferredGatewayTags: parseGatewayTagList(preferredGatewayTagsText),
493
+ preferredGatewayUseCase: preferredGatewayUseCase || null,
466
494
  routingStrategy,
467
495
  routingTargets: routingTargets.map((target, index) => ({
468
496
  ...target,
497
+ preferredGatewayTags: parseGatewayTagList(formatGatewayTagList(target.preferredGatewayTags)),
498
+ preferredGatewayUseCase: target.preferredGatewayUseCase || null,
469
499
  priority: typeof target.priority === 'number' ? target.priority : index + 1,
470
500
  })),
471
501
  subAgentIds: canDelegateToAgents ? subAgentIds : [],
@@ -1698,6 +1728,34 @@ export function AgentSheet() {
1698
1728
  </div>
1699
1729
  )}
1700
1730
 
1731
+ {(provider === 'openclaw' || routingTargets.some((target) => target.provider === 'openclaw') || openclawGatewayProfiles.length > 0) && (
1732
+ <div className="mb-8">
1733
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1734
+ Gateway Preferences <HintTip text="When multiple OpenClaw gateways are available, prefer matching tags or deployment templates before falling back to the default route." />
1735
+ </label>
1736
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1737
+ <input
1738
+ type="text"
1739
+ value={preferredGatewayTagsText}
1740
+ onChange={(e) => setPreferredGatewayTagsText(e.target.value)}
1741
+ placeholder="gpu, local, research"
1742
+ className={inputClass}
1743
+ />
1744
+ <select value={preferredGatewayUseCase} onChange={(e) => setPreferredGatewayUseCase(e.target.value)} className={inputClass}>
1745
+ <option value="">Any OpenClaw template</option>
1746
+ <option value="local-dev">Local Dev</option>
1747
+ <option value="single-vps">Single VPS</option>
1748
+ <option value="private-tailnet">Private Tailnet</option>
1749
+ <option value="browser-heavy">Browser Heavy</option>
1750
+ <option value="team-control">Team Control</option>
1751
+ </select>
1752
+ </div>
1753
+ <p className="text-[11px] text-text-3/70 mt-2">
1754
+ These preferences bias scheduling toward matching OpenClaw control planes without hard-locking the agent to one gateway.
1755
+ </p>
1756
+ </div>
1757
+ )}
1758
+
1701
1759
  <div className="mb-8">
1702
1760
  <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1703
1761
  Model Routing <HintTip text="Route this agent through a provider/model pool instead of a single fixed model. The base provider remains the default when no route matches." />
@@ -1752,25 +1810,45 @@ export function AgentSheet() {
1752
1810
  />
1753
1811
  </div>
1754
1812
  {target.provider === 'openclaw' && openclawGatewayProfiles.length > 0 && (
1755
- <select
1756
- value={target.gatewayProfileId || ''}
1757
- onChange={(e) => {
1758
- const nextId = e.target.value || null
1759
- const gateway = openclawGatewayProfiles.find((item) => item.id === nextId)
1760
- updateRoutingTarget(target.id, {
1761
- gatewayProfileId: nextId,
1762
- apiEndpoint: gateway?.endpoint || target.apiEndpoint || null,
1763
- credentialId: gateway?.credentialId || target.credentialId || null,
1764
- model: target.model || 'default',
1765
- })
1766
- }}
1767
- className={inputClass}
1768
- >
1769
- <option value="">Custom OpenClaw endpoint</option>
1770
- {openclawGatewayProfiles.map((gateway) => (
1771
- <option key={gateway.id} value={gateway.id}>{gateway.name}</option>
1772
- ))}
1773
- </select>
1813
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
1814
+ <select
1815
+ value={target.gatewayProfileId || ''}
1816
+ onChange={(e) => {
1817
+ const nextId = e.target.value || null
1818
+ const gateway = openclawGatewayProfiles.find((item) => item.id === nextId)
1819
+ updateRoutingTarget(target.id, {
1820
+ gatewayProfileId: nextId,
1821
+ apiEndpoint: gateway?.endpoint || target.apiEndpoint || null,
1822
+ credentialId: gateway?.credentialId || target.credentialId || null,
1823
+ model: target.model || 'default',
1824
+ })
1825
+ }}
1826
+ className={inputClass}
1827
+ >
1828
+ <option value="">Custom OpenClaw endpoint</option>
1829
+ {openclawGatewayProfiles.map((gateway) => (
1830
+ <option key={gateway.id} value={gateway.id}>{gateway.name}</option>
1831
+ ))}
1832
+ </select>
1833
+ <input
1834
+ value={formatGatewayTagList(target.preferredGatewayTags)}
1835
+ onChange={(e) => updateRoutingTarget(target.id, { preferredGatewayTags: parseGatewayTagList(e.target.value) })}
1836
+ placeholder="Prefer tags"
1837
+ className={inputClass}
1838
+ />
1839
+ <select
1840
+ value={target.preferredGatewayUseCase || ''}
1841
+ onChange={(e) => updateRoutingTarget(target.id, { preferredGatewayUseCase: e.target.value || null })}
1842
+ className={inputClass}
1843
+ >
1844
+ <option value="">Any OpenClaw template</option>
1845
+ <option value="local-dev">Local Dev</option>
1846
+ <option value="single-vps">Single VPS</option>
1847
+ <option value="private-tailnet">Private Tailnet</option>
1848
+ <option value="browser-heavy">Browser Heavy</option>
1849
+ <option value="team-control">Team Control</option>
1850
+ </select>
1851
+ </div>
1774
1852
  )}
1775
1853
  <div className="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-3">
1776
1854
  <input
@@ -54,6 +54,9 @@ interface ConfiguredProvider {
54
54
  endpoint: string | null
55
55
  defaultModel: string
56
56
  gatewayProfileId: string | null
57
+ notes?: string | null
58
+ tags?: string[]
59
+ deployment?: GatewayProfile['deployment'] | null
57
60
  }
58
61
 
59
62
  interface StarterDraftAgent {
@@ -88,6 +91,20 @@ const CONNECTOR_ICONS = [
88
91
  { name: 'Telegram', icon: 'T' },
89
92
  { name: 'WhatsApp', icon: 'W' },
90
93
  ]
94
+ const OPENCLAW_USE_CASE_LABELS: Record<NonNullable<NonNullable<GatewayProfile['deployment']>['useCase']>, string> = {
95
+ 'local-dev': 'Local Dev',
96
+ 'single-vps': 'Single VPS',
97
+ 'private-tailnet': 'Private Tailnet',
98
+ 'browser-heavy': 'Browser Heavy',
99
+ 'team-control': 'Team Control',
100
+ }
101
+ const OPENCLAW_EXPOSURE_LABELS: Record<NonNullable<NonNullable<GatewayProfile['deployment']>['exposure']>, string> = {
102
+ 'private-lan': 'Private LAN',
103
+ tailscale: 'Tailscale',
104
+ caddy: 'Caddy',
105
+ nginx: 'Nginx',
106
+ 'ssh-tunnel': 'SSH Tunnel',
107
+ }
91
108
 
92
109
  function stepIndex(step: SetupStep): number {
93
110
  if (step === 'connect') return STEP_ORDER.indexOf('providers')
@@ -224,6 +241,12 @@ function ConfiguredProviderChips({ providers }: { providers: ConfiguredProvider[
224
241
  {cp.provider === 'openclaw' && formatEndpointHost(cp.endpoint)
225
242
  ? `· ${formatEndpointHost(cp.endpoint)}`
226
243
  : ''}
244
+ {cp.provider === 'openclaw' && cp.deployment?.useCase
245
+ ? ` · ${OPENCLAW_USE_CASE_LABELS[cp.deployment.useCase]}`
246
+ : ''}
247
+ {cp.provider === 'openclaw' && cp.deployment?.exposure
248
+ ? ` · ${OPENCLAW_EXPOSURE_LABELS[cp.deployment.exposure]}`
249
+ : ''}
227
250
  {cp.defaultModel ? ` · ${cp.defaultModel}` : ''}
228
251
  </span>
229
252
  </span>
@@ -332,6 +355,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
332
355
  const [endpoint, setEndpoint] = useState('')
333
356
  const [apiKey, setApiKey] = useState('')
334
357
  const [credentialId, setCredentialId] = useState<string | null>(null)
358
+ const [providerNotes, setProviderNotes] = useState('')
359
+ const [providerTags, setProviderTags] = useState<string[]>([])
360
+ const [providerDeployment, setProviderDeployment] = useState<GatewayProfile['deployment'] | null>(null)
335
361
  const [checkState, setCheckState] = useState<CheckState>('idle')
336
362
  const [checkMessage, setCheckMessage] = useState('')
337
363
  const [checkErrorCode, setCheckErrorCode] = useState<string | null>(null)
@@ -382,6 +408,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
382
408
  setEndpoint('')
383
409
  setApiKey('')
384
410
  setCredentialId(null)
411
+ setProviderNotes('')
412
+ setProviderTags([])
413
+ setProviderDeployment(null)
385
414
  setCheckState('idle')
386
415
  setCheckMessage('')
387
416
  setCheckErrorCode(null)
@@ -432,6 +461,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
432
461
  setEndpoint(meta?.defaultEndpoint || '')
433
462
  setApiKey('')
434
463
  setCredentialId(null)
464
+ setProviderNotes('')
465
+ setProviderTags([])
466
+ setProviderDeployment(null)
435
467
  setCheckState('idle')
436
468
  setCheckMessage('')
437
469
  setCheckErrorCode(null)
@@ -445,6 +477,8 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
445
477
  endpoint?: string
446
478
  token?: string
447
479
  name?: string
480
+ notes?: string
481
+ deployment?: GatewayProfile['deployment'] | Record<string, unknown> | null
448
482
  }) => {
449
483
  if (patch.endpoint) {
450
484
  setEndpoint(patch.endpoint)
@@ -456,6 +490,22 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
456
490
  if (patch.name && (!providerLabel.trim() || providerLabel.trim() === (selectedProvider?.name || ''))) {
457
491
  setProviderLabel(patch.name)
458
492
  }
493
+ if (patch.notes) {
494
+ setProviderNotes(patch.notes)
495
+ }
496
+ if (patch.deployment) {
497
+ const nextDeployment = patch.deployment as GatewayProfile['deployment']
498
+ setProviderDeployment((current) => ({
499
+ ...(current || {}),
500
+ ...(nextDeployment || {}),
501
+ }))
502
+ setProviderTags((current) => Array.from(new Set([
503
+ ...current,
504
+ 'onboarding',
505
+ ...(nextDeployment?.useCase ? [nextDeployment.useCase] : []),
506
+ ...(nextDeployment?.exposure ? [nextDeployment.exposure] : []),
507
+ ])))
508
+ }
459
509
  setCheckState('idle')
460
510
  setCheckMessage('')
461
511
  setCheckErrorCode(null)
@@ -552,6 +602,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
552
602
  endpoint: supportsEndpoint ? (endpoint.trim() || selectedProvider.defaultEndpoint || null) : null,
553
603
  defaultModel: providerSuggestedModel || getDefaultModelForProvider(provider),
554
604
  gatewayProfileId: null,
605
+ notes: providerNotes.trim() || null,
606
+ tags: providerTags,
607
+ deployment: providerDeployment,
555
608
  }
556
609
 
557
610
  const nextConfigured = [...configuredProviders, configuredProvider]
@@ -645,8 +698,13 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
645
698
  name: configuredProvider.name,
646
699
  endpoint: normalizedEndpoint,
647
700
  credentialId: configuredProvider.credentialId || null,
648
- tags: ['onboarding'],
649
- notes: `Created during setup for ${configuredProvider.name}.`,
701
+ tags: Array.from(new Set([
702
+ 'onboarding',
703
+ ...(configuredProvider.tags || []),
704
+ ])),
705
+ notes: configuredProvider.notes || `Created during setup for ${configuredProvider.name}.`,
706
+ deployment: configuredProvider.deployment || null,
707
+ status: configuredProvider.deployment?.lastVerifiedOk ? 'healthy' : 'pending',
650
708
  isDefault: shouldCreateDefault,
651
709
  })
652
710
  gatewayProfileIdsByProviderConfig.set(configuredProvider.id, createdGateway.id)
@@ -1084,6 +1142,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
1084
1142
  <p className="mt-2 text-[12px] text-text-3 leading-relaxed">
1085
1143
  If you only have a WebSocket gateway URL, you can still paste it here. SwarmClaw will normalize it for agent chat.
1086
1144
  </p>
1145
+ <p className="mt-2 text-[12px] text-text-3 leading-relaxed">
1146
+ Safer remote defaults: use <code className="text-text-2">private-tailnet</code> with <code className="text-text-2">tailscale</code> or <code className="text-text-2">ssh-tunnel</code> unless you intentionally want public HTTPS ingress.
1147
+ </p>
1087
1148
  </div>
1088
1149
  <div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
1089
1150
  <div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Safe defaults</div>
@@ -1093,6 +1154,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
1093
1154
  <p className="mt-2 text-[12px] text-text-3 leading-relaxed">
1094
1155
  Local quickstart uses the bundled official OpenClaw CLI. Remote quickstart uses the official OpenClaw Docker image or the official repo for managed hosts.
1095
1156
  </p>
1157
+ <p className="mt-2 text-[12px] text-text-3 leading-relaxed">
1158
+ Choose <code className="text-text-2">local-dev</code> for one-machine setup, <code className="text-text-2">single-vps</code> for most hosted installs, or <code className="text-text-2">private-tailnet</code> when the gateway should stay private.
1159
+ </p>
1096
1160
  </div>
1097
1161
  </div>
1098
1162
 
@@ -1104,6 +1168,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
1104
1168
  <p className="mt-2 text-[12px] text-text-3 leading-relaxed">
1105
1169
  Current target: <span className="text-text-2">{openClawEndpointHost || 'localhost:18789'}</span>{openClawLocal ? ' · local route' : ' · remote route'}
1106
1170
  </p>
1171
+ <p className="mt-2 text-[12px] text-text-3 leading-relaxed">
1172
+ Use <code className="text-text-2">caddy</code> or <code className="text-text-2">nginx</code> only when you intentionally want HTTPS/public ingress managed on the gateway side.
1173
+ </p>
1107
1174
  </div>
1108
1175
  </div>
1109
1176
  )}
@@ -1314,6 +1381,12 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
1314
1381
  {configuredProvider.provider === 'openclaw' && formatEndpointHost(configuredProvider.endpoint)
1315
1382
  ? ` · ${formatEndpointHost(configuredProvider.endpoint)}`
1316
1383
  : ''}
1384
+ {configuredProvider.provider === 'openclaw' && configuredProvider.deployment?.useCase
1385
+ ? ` · ${OPENCLAW_USE_CASE_LABELS[configuredProvider.deployment.useCase]}`
1386
+ : ''}
1387
+ {configuredProvider.provider === 'openclaw' && configuredProvider.deployment?.exposure
1388
+ ? ` · ${OPENCLAW_EXPOSURE_LABELS[configuredProvider.deployment.exposure]}`
1389
+ : ''}
1317
1390
  {configuredProvider.defaultModel ? ` · ${configuredProvider.defaultModel}` : ''}
1318
1391
  </option>
1319
1392
  ))}