@swarmclawai/swarmclaw 1.9.2 → 1.9.4
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 +23 -3
- package/electron-dist/main.js +218 -0
- package/package.json +2 -2
- package/src/app/api/extensions/managed-resources/route.test.ts +117 -0
- package/src/app/api/extensions/managed-resources/route.ts +116 -0
- package/src/app/api/gateways/[id]/environments/[environmentId]/route.ts +16 -0
- package/src/app/api/gateways/[id]/environments/route.ts +13 -0
- package/src/app/api/gateways/topology-route.test.ts +30 -0
- package/src/app/api/tasks/task-workspace-route.test.ts +4 -0
- package/src/cli/index.js +4 -0
- package/src/cli/spec.js +4 -0
- package/src/components/providers/provider-list.tsx +34 -1
- package/src/components/tasks/task-sheet.tsx +50 -0
- package/src/features/gateways/queries.ts +3 -0
- package/src/lib/server/extension-managed-resources.test.ts +159 -0
- package/src/lib/server/extension-managed-resources.ts +905 -0
- package/src/lib/server/extensions.ts +113 -2
- package/src/lib/server/gateways/gateway-profile-service.ts +2 -0
- package/src/lib/server/gateways/gateway-topology.test.ts +59 -3
- package/src/lib/server/gateways/gateway-topology.ts +129 -3
- package/src/lib/server/operations/operation-pulse.test.ts +29 -0
- package/src/lib/server/operations/operation-pulse.ts +9 -0
- package/src/lib/server/session-tools/extension-creator.ts +50 -0
- package/src/lib/server/tasks/task-execution-workspace.test.ts +14 -0
- package/src/lib/server/tasks/task-execution-workspace.ts +133 -6
- package/src/types/agent.ts +2 -0
- package/src/types/app-settings.ts +8 -0
- package/src/types/extension.ts +132 -0
- package/src/types/misc.ts +31 -0
- package/src/types/schedule.ts +3 -0
- package/src/types/task.ts +30 -0
- package/src/views/settings/extension-manager.tsx +157 -1
|
@@ -475,10 +475,14 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
475
475
|
<div className="text-[12px] font-800 text-text">Gateway fleet topology</div>
|
|
476
476
|
<div className="mt-1 text-[11px] text-text-3/70">
|
|
477
477
|
{gatewayFleetTopology.totals.connectedGatewayCount}/{gatewayFleetTopology.totals.gatewayCount} gateways connected ·{' '}
|
|
478
|
-
{gatewayFleetTopology.totals.connectedNodeCount}/{gatewayFleetTopology.totals.nodeCount} nodes connected
|
|
478
|
+
{gatewayFleetTopology.totals.connectedNodeCount}/{gatewayFleetTopology.totals.nodeCount} nodes connected ·{' '}
|
|
479
|
+
{gatewayFleetTopology.totals.availableEnvironmentCount || 0}/{gatewayFleetTopology.totals.environmentCount || 0} environments available
|
|
479
480
|
</div>
|
|
480
481
|
</div>
|
|
481
482
|
<div className="flex flex-wrap gap-2 text-[11px] text-text-3/70">
|
|
483
|
+
<span className="rounded-full border border-white/[0.06] bg-white/[0.03] px-2.5 py-1">
|
|
484
|
+
{gatewayFleetTopology.totals.environmentCount || 0} environments
|
|
485
|
+
</span>
|
|
482
486
|
<span className="rounded-full border border-white/[0.06] bg-white/[0.03] px-2.5 py-1">
|
|
483
487
|
{gatewayFleetTopology.totals.sessionCount || 0} sessions
|
|
484
488
|
</span>
|
|
@@ -536,6 +540,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
536
540
|
const topologyErrors = topology?.errors || []
|
|
537
541
|
const pendingPairings = (stats?.pendingNodePairings || 0) + (stats?.pendingDevicePairings || 0)
|
|
538
542
|
const topologyErrorCount = topologyErrors.length || stats?.lastTopologyErrorCount || 0
|
|
543
|
+
const environments = topology?.environments || []
|
|
539
544
|
return (
|
|
540
545
|
<div
|
|
541
546
|
key={gateway.id}
|
|
@@ -602,6 +607,12 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
602
607
|
{stats?.connectedNodeCount ?? 0}/{stats?.nodeCount ?? 0} nodes · {stats?.pairedDeviceCount ?? 0} devices
|
|
603
608
|
</div>
|
|
604
609
|
</div>
|
|
610
|
+
<div className="rounded-[10px] border border-white/[0.05] bg-white/[0.02] px-3 py-2">
|
|
611
|
+
<div className="uppercase tracking-[0.08em] text-text-3/50">Environments</div>
|
|
612
|
+
<div className="mt-1 text-text-2">
|
|
613
|
+
{stats?.availableEnvironmentCount ?? 0}/{stats?.environmentCount ?? 0} available
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
605
616
|
<div className="rounded-[10px] border border-white/[0.05] bg-white/[0.02] px-3 py-2">
|
|
606
617
|
<div className="uppercase tracking-[0.08em] text-text-3/50">Sessions</div>
|
|
607
618
|
<div className="mt-1 text-text-2">
|
|
@@ -616,6 +627,28 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
616
627
|
</div>
|
|
617
628
|
</div>
|
|
618
629
|
)}
|
|
630
|
+
{!inSidebar && environments.length > 0 && (
|
|
631
|
+
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
632
|
+
{environments.slice(0, 4).map((environment) => (
|
|
633
|
+
<span
|
|
634
|
+
key={`${gateway.id}-${environment.id}`}
|
|
635
|
+
className={`rounded-full border px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.08em] ${
|
|
636
|
+
environment.status === 'available'
|
|
637
|
+
? 'border-emerald-400/20 bg-emerald-400/[0.06] text-emerald-300'
|
|
638
|
+
: 'border-white/[0.06] bg-white/[0.03] text-text-3/70'
|
|
639
|
+
}`}
|
|
640
|
+
title={environment.capabilities?.join(', ') || environment.id}
|
|
641
|
+
>
|
|
642
|
+
{environment.label || environment.id}
|
|
643
|
+
</span>
|
|
644
|
+
))}
|
|
645
|
+
{environments.length > 4 && (
|
|
646
|
+
<span className="rounded-full border border-white/[0.06] bg-white/[0.03] px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/70">
|
|
647
|
+
+{environments.length - 4}
|
|
648
|
+
</span>
|
|
649
|
+
)}
|
|
650
|
+
</div>
|
|
651
|
+
)}
|
|
619
652
|
{!inSidebar && (pendingPairings > 0 || topologyErrorCount > 0) && (
|
|
620
653
|
<div className={`mt-3 rounded-[10px] border px-3 py-2 text-[11px] leading-relaxed ${
|
|
621
654
|
topologyErrorCount > 0
|
|
@@ -462,6 +462,31 @@ export function TaskSheet() {
|
|
|
462
462
|
{editing.executionWorkspace?.path && (
|
|
463
463
|
<code className="block text-[12px] text-text-3 font-mono break-all">{editing.executionWorkspace.path}</code>
|
|
464
464
|
)}
|
|
465
|
+
{(editing.executionWorkspace?.contextPath || editing.executionWorkspace?.envPath) && (
|
|
466
|
+
<div className="grid grid-cols-1 gap-2 text-[11px] text-text-3/70">
|
|
467
|
+
{editing.executionWorkspace.contextPath && (
|
|
468
|
+
<div className="rounded-[10px] border border-white/[0.05] bg-white/[0.02] px-3 py-2">
|
|
469
|
+
<div className="uppercase tracking-[0.08em] text-text-3/50">Context</div>
|
|
470
|
+
<code className="mt-1 block break-all text-text-2">{editing.executionWorkspace.contextPath}</code>
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
{editing.executionWorkspace.envPath && (
|
|
474
|
+
<div className="rounded-[10px] border border-white/[0.05] bg-white/[0.02] px-3 py-2">
|
|
475
|
+
<div className="uppercase tracking-[0.08em] text-text-3/50">Env</div>
|
|
476
|
+
<code className="mt-1 block break-all text-text-2">{editing.executionWorkspace.envPath}</code>
|
|
477
|
+
</div>
|
|
478
|
+
)}
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
{editing.executionWorkspace?.envHints?.length ? (
|
|
482
|
+
<div className="flex flex-wrap gap-1.5">
|
|
483
|
+
{editing.executionWorkspace.envHints.slice(0, 8).map((hint) => (
|
|
484
|
+
<InfoChip key={hint.key} tone="neutral" title={hint.value}>
|
|
485
|
+
<span className="max-w-[220px] truncate">{hint.key}</span>
|
|
486
|
+
</InfoChip>
|
|
487
|
+
))}
|
|
488
|
+
</div>
|
|
489
|
+
) : null}
|
|
465
490
|
{previewLinks.length > 0 && (
|
|
466
491
|
<div className="flex flex-wrap gap-2">
|
|
467
492
|
{previewLinks.map((link) => (
|
|
@@ -942,6 +967,31 @@ export function TaskSheet() {
|
|
|
942
967
|
{editing.executionWorkspace?.path && (
|
|
943
968
|
<code className="block text-[12px] text-text-3 font-mono break-all">{editing.executionWorkspace.path}</code>
|
|
944
969
|
)}
|
|
970
|
+
{(editing.executionWorkspace?.contextPath || editing.executionWorkspace?.envPath) && (
|
|
971
|
+
<div className="grid grid-cols-1 gap-2 text-[11px] text-text-3/70">
|
|
972
|
+
{editing.executionWorkspace.contextPath && (
|
|
973
|
+
<div className="rounded-[10px] border border-white/[0.05] bg-white/[0.02] px-3 py-2">
|
|
974
|
+
<div className="uppercase tracking-[0.08em] text-text-3/50">Context</div>
|
|
975
|
+
<code className="mt-1 block break-all text-text-2">{editing.executionWorkspace.contextPath}</code>
|
|
976
|
+
</div>
|
|
977
|
+
)}
|
|
978
|
+
{editing.executionWorkspace.envPath && (
|
|
979
|
+
<div className="rounded-[10px] border border-white/[0.05] bg-white/[0.02] px-3 py-2">
|
|
980
|
+
<div className="uppercase tracking-[0.08em] text-text-3/50">Env</div>
|
|
981
|
+
<code className="mt-1 block break-all text-text-2">{editing.executionWorkspace.envPath}</code>
|
|
982
|
+
</div>
|
|
983
|
+
)}
|
|
984
|
+
</div>
|
|
985
|
+
)}
|
|
986
|
+
{editing.executionWorkspace?.envHints?.length ? (
|
|
987
|
+
<div className="flex flex-wrap gap-1.5">
|
|
988
|
+
{editing.executionWorkspace.envHints.slice(0, 8).map((hint) => (
|
|
989
|
+
<InfoChip key={hint.key} tone="neutral" title={hint.value}>
|
|
990
|
+
<span className="max-w-[220px] truncate">{hint.key}</span>
|
|
991
|
+
</InfoChip>
|
|
992
|
+
))}
|
|
993
|
+
</div>
|
|
994
|
+
) : null}
|
|
945
995
|
{previewLinks.length > 0 && (
|
|
946
996
|
<div className="flex flex-wrap gap-2">
|
|
947
997
|
{previewLinks.map((link) => (
|
|
@@ -3,6 +3,7 @@ import { api } from '@/lib/app/api-client'
|
|
|
3
3
|
import { credentialQueryKeys } from '@/features/credentials/queries'
|
|
4
4
|
import type {
|
|
5
5
|
GatewayProfile,
|
|
6
|
+
OpenClawEnvironmentSummary,
|
|
6
7
|
OpenClawDevicePairRequest,
|
|
7
8
|
OpenClawGatewayFleetTopology,
|
|
8
9
|
OpenClawGatewayPresenceEntry,
|
|
@@ -60,6 +61,7 @@ export interface RefreshGatewayTopologyResult {
|
|
|
60
61
|
pairedDevices: OpenClawPairedDevice[]
|
|
61
62
|
sessions: OpenClawGatewaySession[]
|
|
62
63
|
presence: OpenClawGatewayPresenceEntry[]
|
|
64
|
+
environments: OpenClawEnvironmentSummary[]
|
|
63
65
|
errors: OpenClawGatewayRpcError[]
|
|
64
66
|
topology: OpenClawGatewayTopology
|
|
65
67
|
}
|
|
@@ -190,6 +192,7 @@ export function useRefreshGatewayTopologyMutation() {
|
|
|
190
192
|
pairedDevices: topology.pairedDevices,
|
|
191
193
|
sessions: topology.sessions,
|
|
192
194
|
presence: topology.presence,
|
|
195
|
+
environments: topology.environments,
|
|
193
196
|
errors: topology.errors,
|
|
194
197
|
topology,
|
|
195
198
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { afterEach } from 'node:test'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import os from 'node:os'
|
|
6
|
+
|
|
7
|
+
import { getExtensionManager } from './extensions'
|
|
8
|
+
import {
|
|
9
|
+
inspectExtensionLocalFolder,
|
|
10
|
+
listExtensionLocalFolderEntries,
|
|
11
|
+
listExtensionManagedResources,
|
|
12
|
+
reconcileExtensionManagedResources,
|
|
13
|
+
setExtensionLocalFolderConfig,
|
|
14
|
+
} from './extension-managed-resources'
|
|
15
|
+
import { loadAgents, loadSchedules, loadSettings, saveAgents, saveSchedules, saveSettings } from './storage'
|
|
16
|
+
|
|
17
|
+
const originalAgents = loadAgents()
|
|
18
|
+
const originalSchedules = loadSchedules()
|
|
19
|
+
const originalSettings = loadSettings()
|
|
20
|
+
|
|
21
|
+
let seq = 0
|
|
22
|
+
|
|
23
|
+
function extensionId(prefix: string): string {
|
|
24
|
+
seq += 1
|
|
25
|
+
return `${prefix}_${Date.now()}_${seq}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
saveAgents(originalAgents)
|
|
30
|
+
saveSchedules(originalSchedules)
|
|
31
|
+
saveSettings(originalSettings)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('managed resources summary and reconcile create extension-owned agents and schedules', () => {
|
|
35
|
+
const id = extensionId('managed_resources')
|
|
36
|
+
getExtensionManager().registerBuiltin(id, {
|
|
37
|
+
name: 'Managed Resource Fixture',
|
|
38
|
+
description: 'Declares resources for tests.',
|
|
39
|
+
managedResources: {
|
|
40
|
+
agents: [
|
|
41
|
+
{
|
|
42
|
+
agentKey: 'researcher',
|
|
43
|
+
displayName: 'Managed Researcher',
|
|
44
|
+
description: 'Research managed by an extension.',
|
|
45
|
+
systemPrompt: 'Research carefully.',
|
|
46
|
+
provider: 'openai',
|
|
47
|
+
model: 'gpt-4o-mini',
|
|
48
|
+
capabilities: ['research'],
|
|
49
|
+
extensions: ['web'],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
routines: [
|
|
53
|
+
{
|
|
54
|
+
routineKey: 'daily-digest',
|
|
55
|
+
title: 'Daily Digest',
|
|
56
|
+
assigneeRef: { resourceKind: 'agent', resourceKey: 'researcher' },
|
|
57
|
+
taskPrompt: 'Prepare a digest.',
|
|
58
|
+
triggers: [{ kind: 'schedule', cronExpression: '0 9 * * *', timezone: 'UTC' }],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
gatewayPlatforms: [
|
|
62
|
+
{
|
|
63
|
+
platformKey: 'openai-api',
|
|
64
|
+
displayName: 'OpenAI-compatible API',
|
|
65
|
+
transport: 'http',
|
|
66
|
+
endpoint: 'http://127.0.0.1:8642/v1',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
setupChecks: [
|
|
70
|
+
{ checkKey: 'api-key', displayName: 'API key configured', kind: 'env', target: 'OPENAI_API_KEY' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const before = listExtensionManagedResources().extensions.find((entry) => entry.extensionId === id)
|
|
76
|
+
assert.ok(before)
|
|
77
|
+
assert.equal(before.agents[0].status, 'missing')
|
|
78
|
+
assert.equal(before.schedules[0].status, 'missing_ref')
|
|
79
|
+
|
|
80
|
+
const result = reconcileExtensionManagedResources(id)
|
|
81
|
+
assert.equal(result.createdAgents.length, 1)
|
|
82
|
+
assert.equal(result.createdSchedules.length, 1)
|
|
83
|
+
assert.deepEqual(result.skipped, [])
|
|
84
|
+
|
|
85
|
+
const agents = loadAgents()
|
|
86
|
+
const agent = agents[result.createdAgents[0]]
|
|
87
|
+
assert.equal(agent.name, 'Managed Researcher')
|
|
88
|
+
assert.equal(agent.managedByExtension?.extensionId, id)
|
|
89
|
+
assert.equal(agent.managedByExtension?.resourceKey, 'researcher')
|
|
90
|
+
assert.ok(agent.extensions?.includes(id))
|
|
91
|
+
assert.ok(agent.extensions?.includes('web'))
|
|
92
|
+
|
|
93
|
+
const schedules = loadSchedules()
|
|
94
|
+
const schedule = schedules[result.createdSchedules[0]]
|
|
95
|
+
assert.equal(schedule.name, 'Daily Digest')
|
|
96
|
+
assert.equal(schedule.agentId, agent.id)
|
|
97
|
+
assert.equal(schedule.scheduleType, 'cron')
|
|
98
|
+
assert.equal(schedule.cron, '0 9 * * *')
|
|
99
|
+
assert.equal(schedule.status, 'paused')
|
|
100
|
+
assert.equal(schedule.managedByExtension?.resourceKey, 'daily-digest')
|
|
101
|
+
|
|
102
|
+
const after = listExtensionManagedResources().extensions.find((entry) => entry.extensionId === id)
|
|
103
|
+
assert.equal(after?.agents[0].status, 'resolved')
|
|
104
|
+
assert.equal(after?.schedules[0].status, 'resolved')
|
|
105
|
+
assert.equal(after?.gatewayPlatforms.length, 1)
|
|
106
|
+
assert.equal(after?.setupChecks.length, 1)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('local folder inspection and listing stay inside configured roots', async () => {
|
|
110
|
+
const id = extensionId('managed_folder')
|
|
111
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-managed-folder-'))
|
|
112
|
+
fs.mkdirSync(path.join(tempDir, 'inputs'))
|
|
113
|
+
fs.mkdirSync(path.join(tempDir, 'outputs'))
|
|
114
|
+
fs.writeFileSync(path.join(tempDir, 'inputs', 'brief.txt'), 'hello\n')
|
|
115
|
+
|
|
116
|
+
getExtensionManager().registerBuiltin(id, {
|
|
117
|
+
name: 'Managed Folder Fixture',
|
|
118
|
+
managedResources: {
|
|
119
|
+
localFolders: [
|
|
120
|
+
{
|
|
121
|
+
folderKey: 'workspace',
|
|
122
|
+
displayName: 'Workspace Folder',
|
|
123
|
+
access: 'readWrite',
|
|
124
|
+
requiredDirectories: ['inputs', 'outputs'],
|
|
125
|
+
requiredFiles: ['inputs/brief.txt'],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
setExtensionLocalFolderConfig({
|
|
132
|
+
extensionId: id,
|
|
133
|
+
folderKey: 'workspace',
|
|
134
|
+
path: tempDir,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const status = await inspectExtensionLocalFolder({ extensionId: id, folderKey: 'workspace' })
|
|
138
|
+
assert.equal(status.healthy, true)
|
|
139
|
+
assert.equal(status.readable, true)
|
|
140
|
+
assert.equal(status.writable, true)
|
|
141
|
+
|
|
142
|
+
const listing = await listExtensionLocalFolderEntries({
|
|
143
|
+
extensionId: id,
|
|
144
|
+
folderKey: 'workspace',
|
|
145
|
+
recursive: true,
|
|
146
|
+
})
|
|
147
|
+
assert.ok(listing.entries.some((entry) => entry.path === 'inputs/brief.txt' && entry.kind === 'file'))
|
|
148
|
+
|
|
149
|
+
await assert.rejects(
|
|
150
|
+
() => listExtensionLocalFolderEntries({
|
|
151
|
+
extensionId: id,
|
|
152
|
+
folderKey: 'workspace',
|
|
153
|
+
relativePath: '../outside',
|
|
154
|
+
}),
|
|
155
|
+
/inside the configured root|traversal/,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
159
|
+
})
|