@swarmclawai/swarmclaw 1.9.2 → 1.9.3
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 +11 -1
- 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/cli/index.js +2 -0
- package/src/cli/spec.js +2 -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/session-tools/extension-creator.ts +50 -0
- 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/schedule.ts +3 -0
- package/src/views/settings/extension-manager.tsx +157 -1
|
@@ -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
|
+
})
|