@swarmclawai/swarmclaw 1.2.8 → 1.2.9
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 +30 -6
- package/package.json +2 -2
- package/src/app/agents/[id]/page.tsx +1 -18
- package/src/app/api/agents/thread-route.test.ts +0 -1
- package/src/app/api/approvals/route.test.ts +6 -22
- package/src/app/api/connectors/route.ts +2 -2
- package/src/app/api/portability/export/route.ts +8 -0
- package/src/app/api/portability/import/route.test.ts +80 -0
- package/src/app/api/portability/import/route.ts +28 -0
- package/src/app/api/settings/route.ts +0 -2
- package/src/app/api/wallets/[id]/route.ts +15 -157
- package/src/app/api/wallets/generate/route.ts +22 -0
- package/src/app/api/wallets/route.test.ts +147 -0
- package/src/app/api/wallets/route.ts +13 -95
- package/src/app/autonomy/page.tsx +2 -57
- package/src/app/protocols/page.tsx +2 -21
- package/src/app/settings/page.tsx +0 -9
- package/src/app/wallets/page.tsx +105 -5
- package/src/cli/index.js +21 -33
- package/src/cli/spec.js +19 -30
- package/src/components/agents/agent-sheet.tsx +2 -40
- package/src/components/agents/inspector-panel.tsx +0 -83
- package/src/components/chat/chat-card.tsx +0 -31
- package/src/components/chat/message-bubble.tsx +1 -108
- package/src/components/connectors/connector-sheet.tsx +25 -1
- package/src/components/layout/sidebar-rail.tsx +6 -10
- package/src/components/projects/project-detail.tsx +3 -35
- package/src/components/projects/tabs/overview-tab.tsx +3 -59
- package/src/components/projects/tabs/work-tab.tsx +7 -77
- package/src/components/protocols/structured-session-launcher.tsx +1 -22
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/tasks/task-card.tsx +4 -34
- package/src/components/tasks/task-sheet.tsx +6 -36
- package/src/components/wallets/wallet-list.tsx +150 -0
- package/src/lib/app/navigation.test.ts +0 -13
- package/src/lib/app/navigation.ts +2 -7
- package/src/lib/app/view-constants.ts +14 -19
- package/src/lib/server/agents/agent-thread-session.ts +0 -1
- package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
- package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
- package/src/lib/server/agents/delegation-jobs.ts +0 -25
- package/src/lib/server/agents/main-agent-loop.ts +1 -49
- package/src/lib/server/agents/subagent-runtime.ts +0 -1
- package/src/lib/server/approval-match.ts +0 -85
- package/src/lib/server/approvals.test.ts +6 -6
- package/src/lib/server/approvals.ts +0 -6
- package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
- package/src/lib/server/builtin-extensions.ts +0 -2
- package/src/lib/server/capability-router.test.ts +0 -2
- package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +14 -14
- package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
- package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -2
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +2 -22
- package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
- package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
- package/src/lib/server/chat-execution/message-classifier.ts +1 -16
- package/src/lib/server/chat-execution/prompt-builder.test.ts +0 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +0 -30
- package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
- package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
- package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +8 -123
- package/src/lib/server/chat-execution/stream-agent-chat.ts +1 -5
- package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
- package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
- package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
- package/src/lib/server/chats/chat-session-service.ts +3 -5
- package/src/lib/server/connectors/connector-inbound.ts +0 -1
- package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
- package/src/lib/server/connectors/connector-service.ts +39 -9
- package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
- package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
- package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
- package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
- package/src/lib/server/connectors/swarmdock-tasks.ts +119 -0
- package/src/lib/server/connectors/swarmdock.ts +255 -0
- package/src/lib/server/execution-brief.test.ts +2 -25
- package/src/lib/server/execution-brief.ts +12 -35
- package/src/lib/server/execution-engine/task-attempt.ts +0 -1
- package/src/lib/server/persistence/storage-context.ts +0 -5
- package/src/lib/server/portability/export.ts +109 -0
- package/src/lib/server/portability/import.ts +159 -0
- package/src/lib/server/protocols/protocol-normalization.ts +0 -4
- package/src/lib/server/protocols/protocol-queries.ts +0 -6
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
- package/src/lib/server/protocols/protocol-service.ts +0 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
- package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
- package/src/lib/server/protocols/protocol-swarm.ts +0 -2
- package/src/lib/server/protocols/protocol-types.ts +0 -2
- package/src/lib/server/provider-health.ts +0 -9
- package/src/lib/server/runtime/daemon-state/core.ts +0 -9
- package/src/lib/server/runtime/daemon-state.test.ts +0 -35
- package/src/lib/server/runtime/heartbeat-service.ts +3 -23
- package/src/lib/server/runtime/queue/core.ts +11 -33
- package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
- package/src/lib/server/runtime/scheduler.ts +0 -13
- package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
- package/src/lib/server/runtime/session-run-manager/queries.ts +0 -1
- package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
- package/src/lib/server/runtime/session-run-manager.test.ts +0 -28
- package/src/lib/server/session-tools/crud.ts +0 -14
- package/src/lib/server/session-tools/delegate.ts +0 -4
- package/src/lib/server/session-tools/index.ts +0 -4
- package/src/lib/server/session-tools/team-context.ts +0 -3
- package/src/lib/server/storage-normalization.ts +8 -0
- package/src/lib/server/storage.ts +18 -45
- package/src/lib/server/tasks/task-checkout.ts +59 -0
- package/src/lib/server/tasks/task-lifecycle.ts +2 -0
- package/src/lib/server/tasks/task-route-service.ts +4 -26
- package/src/lib/server/tasks/task-service.ts +0 -7
- package/src/lib/server/tool-aliases.ts +0 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +4 -4
- package/src/lib/server/tool-capability-policy.ts +0 -2
- package/src/lib/server/tool-planning.ts +0 -12
- package/src/lib/server/universal-tool-access.ts +0 -1
- package/src/lib/server/wallets/wallet-crypto.ts +33 -0
- package/src/lib/server/wallets/wallet-repository.ts +24 -0
- package/src/lib/server/wallets/wallet-service.ts +119 -0
- package/src/lib/server/working-state/extraction.ts +8 -42
- package/src/lib/server/working-state/normalization.ts +10 -103
- package/src/lib/server/working-state/service.ts +12 -21
- package/src/lib/strip-internal-metadata.test.ts +1 -1
- package/src/lib/strip-internal-metadata.ts +1 -1
- package/src/lib/tool-definitions.ts +0 -1
- package/src/lib/validation/schemas.ts +33 -2
- package/src/stores/slices/data-slice.ts +5 -1
- package/src/stores/slices/ui-slice.ts +0 -4
- package/src/types/agent.ts +0 -84
- package/src/types/app-settings.ts +0 -2
- package/src/types/approval.ts +0 -2
- package/src/types/connector.ts +1 -0
- package/src/types/index.ts +1 -1
- package/src/types/message.ts +0 -1
- package/src/types/misc.ts +0 -2
- package/src/types/protocol.ts +0 -2
- package/src/types/run.ts +0 -3
- package/src/types/session.ts +1 -51
- package/src/types/swarmdock.ts +29 -0
- package/src/types/task.ts +7 -3
- package/src/types/working-state.ts +2 -9
- package/src/views/settings/section-runtime-loop.tsx +0 -14
- package/src/app/api/canvas/[sessionId]/route.ts +0 -35
- package/src/app/api/missions/[id]/actions/route.ts +0 -31
- package/src/app/api/missions/[id]/events/route.ts +0 -14
- package/src/app/api/missions/[id]/route.ts +0 -10
- package/src/app/api/missions/route.test.ts +0 -244
- package/src/app/api/missions/route.ts +0 -57
- package/src/app/api/wallets/[id]/approve/route.ts +0 -79
- package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
- package/src/app/api/wallets/[id]/send/route.ts +0 -113
- package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
- package/src/app/missions/[id]/page.tsx +0 -3
- package/src/app/missions/page.tsx +0 -685
- package/src/components/canvas/canvas-panel.tsx +0 -267
- package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
- package/src/components/wallets/wallet-panel.tsx +0 -1010
- package/src/components/wallets/wallet-section.tsx +0 -260
- package/src/features/missions/queries.ts +0 -23
- package/src/lib/canvas-content.test.ts +0 -360
- package/src/lib/canvas-content.ts +0 -198
- package/src/lib/server/canvas-content.test.ts +0 -32
- package/src/lib/server/canvas-content.ts +0 -6
- package/src/lib/server/ethereum.ts +0 -591
- package/src/lib/server/evm-swap.ts +0 -476
- package/src/lib/server/missions/mission-intent.test.ts +0 -63
- package/src/lib/server/missions/mission-intent.ts +0 -569
- package/src/lib/server/missions/mission-repository.ts +0 -74
- package/src/lib/server/missions/mission-service/actions.ts +0 -6
- package/src/lib/server/missions/mission-service/bindings.ts +0 -9
- package/src/lib/server/missions/mission-service/context.ts +0 -4
- package/src/lib/server/missions/mission-service/core.ts +0 -2271
- package/src/lib/server/missions/mission-service/queries.ts +0 -12
- package/src/lib/server/missions/mission-service/recovery.ts +0 -5
- package/src/lib/server/missions/mission-service/ticks.ts +0 -9
- package/src/lib/server/missions/mission-service.test.ts +0 -888
- package/src/lib/server/missions/mission-service.ts +0 -6
- package/src/lib/server/session-tools/canvas.ts +0 -105
- package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
- package/src/lib/server/session-tools/wallet.ts +0 -1287
- package/src/lib/server/solana.ts +0 -327
- package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
- package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
- package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
- package/src/lib/server/wallet/wallet-service.test.ts +0 -81
- package/src/lib/server/wallet/wallet-service.ts +0 -225
- package/src/lib/wallet/wallet-transactions.test.ts +0 -75
- package/src/lib/wallet/wallet-transactions.ts +0 -43
- package/src/lib/wallet/wallet.test.ts +0 -333
- package/src/lib/wallet/wallet.ts +0 -183
- package/src/types/mission.ts +0 -185
- package/src/views/settings/section-wallets.tsx +0 -35
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import test from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir(script: string) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-swarmdock-secret-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
CREDENTIAL_SECRET: 'test-credential-secret',
|
|
18
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
19
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
20
|
+
},
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
})
|
|
23
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
24
|
+
const lines = (result.stdout || '')
|
|
25
|
+
.trim()
|
|
26
|
+
.split('\n')
|
|
27
|
+
.map((line) => line.trim())
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
30
|
+
return JSON.parse(jsonLine || '{}')
|
|
31
|
+
} finally {
|
|
32
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('createConnector stores SwarmDock private keys as credentials and redacts config output', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
39
|
+
const serviceMod = await import('./src/lib/server/connectors/connector-service')
|
|
40
|
+
const secretMod = await import('./src/lib/server/connectors/swarmdock-secret')
|
|
41
|
+
const storage = storageMod.default || storageMod
|
|
42
|
+
const service = serviceMod.default || serviceMod
|
|
43
|
+
const secret = secretMod.default || secretMod
|
|
44
|
+
|
|
45
|
+
const created = service.createConnector({
|
|
46
|
+
name: 'SwarmDock Worker',
|
|
47
|
+
platform: 'swarmdock',
|
|
48
|
+
config: {
|
|
49
|
+
apiUrl: 'https://api.swarmdock.example',
|
|
50
|
+
walletAddress: '0x000000000000000000000000000000000000dEaD',
|
|
51
|
+
privateKey: 'legacy-private-key',
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const stored = storage.loadConnectors()[created.id]
|
|
56
|
+
const credentials = storage.loadCredentials()
|
|
57
|
+
const credential = stored?.credentialId ? credentials[stored.credentialId] : null
|
|
58
|
+
const redacted = secret.redactConnectorSecrets({
|
|
59
|
+
...stored,
|
|
60
|
+
config: {
|
|
61
|
+
...(stored?.config || {}),
|
|
62
|
+
privateKey: 'should-not-leak',
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
console.log(JSON.stringify({
|
|
67
|
+
credentialId: stored?.credentialId || null,
|
|
68
|
+
storedHasPrivateKey: Boolean(stored?.config && Object.prototype.hasOwnProperty.call(stored.config, 'privateKey')),
|
|
69
|
+
credentialProvider: credential?.provider || null,
|
|
70
|
+
credentialName: credential?.name || null,
|
|
71
|
+
redactedHasPrivateKey: Boolean(redacted?.config && Object.prototype.hasOwnProperty.call(redacted.config, 'privateKey')),
|
|
72
|
+
}))
|
|
73
|
+
`)
|
|
74
|
+
|
|
75
|
+
assert.match(String(output.credentialId || ''), /^cred_/)
|
|
76
|
+
assert.equal(output.storedHasPrivateKey, false)
|
|
77
|
+
assert.equal(output.credentialProvider, 'swarmdock')
|
|
78
|
+
assert.match(String(output.credentialName || ''), /SwarmDock Identity Key/)
|
|
79
|
+
assert.equal(output.redactedHasPrivateKey, false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('ensureSwarmdockConnectorCredential migrates stored legacy config keys into credentials', () => {
|
|
83
|
+
const output = runWithTempDataDir(`
|
|
84
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
85
|
+
const repoMod = await import('./src/lib/server/connectors/connector-repository')
|
|
86
|
+
const secretMod = await import('./src/lib/server/connectors/swarmdock-secret')
|
|
87
|
+
const storage = storageMod.default || storageMod
|
|
88
|
+
const repo = repoMod.default || repoMod
|
|
89
|
+
const secret = secretMod.default || secretMod
|
|
90
|
+
|
|
91
|
+
storage.saveConnectors({
|
|
92
|
+
conn_legacy: {
|
|
93
|
+
id: 'conn_legacy',
|
|
94
|
+
name: 'Legacy SwarmDock',
|
|
95
|
+
platform: 'swarmdock',
|
|
96
|
+
agentId: null,
|
|
97
|
+
chatroomId: null,
|
|
98
|
+
credentialId: null,
|
|
99
|
+
config: {
|
|
100
|
+
walletAddress: '0x000000000000000000000000000000000000dEaD',
|
|
101
|
+
privateKey: 'legacy-private-key',
|
|
102
|
+
},
|
|
103
|
+
isEnabled: false,
|
|
104
|
+
status: 'stopped',
|
|
105
|
+
lastError: null,
|
|
106
|
+
createdAt: 1,
|
|
107
|
+
updatedAt: 1,
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const prepared = secret.ensureSwarmdockConnectorCredential(repo.loadConnector('conn_legacy'))
|
|
112
|
+
const migrated = repo.loadConnector('conn_legacy')
|
|
113
|
+
const credentials = storage.loadCredentials()
|
|
114
|
+
const credential = migrated?.credentialId ? credentials[migrated.credentialId] : null
|
|
115
|
+
|
|
116
|
+
console.log(JSON.stringify({
|
|
117
|
+
fallbackPrivateKey: prepared.fallbackPrivateKey,
|
|
118
|
+
migratedCredentialId: migrated?.credentialId || null,
|
|
119
|
+
migratedHasPrivateKey: Boolean(migrated?.config && Object.prototype.hasOwnProperty.call(migrated.config, 'privateKey')),
|
|
120
|
+
credentialProvider: credential?.provider || null,
|
|
121
|
+
}))
|
|
122
|
+
`)
|
|
123
|
+
|
|
124
|
+
assert.equal(output.fallbackPrivateKey, null)
|
|
125
|
+
assert.match(String(output.migratedCredentialId || ''), /^cred_/)
|
|
126
|
+
assert.equal(output.migratedHasPrivateKey, false)
|
|
127
|
+
assert.equal(output.credentialProvider, 'swarmdock')
|
|
128
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Connector } from '@/types'
|
|
2
|
+
import { createCredentialRecord, resolveCredentialSecret } from '@/lib/server/credentials/credential-service'
|
|
3
|
+
import { upsertConnector } from './connector-repository'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
|
|
6
|
+
export const SWARMMDOCK_CREDENTIAL_PROVIDER = 'swarmdock'
|
|
7
|
+
|
|
8
|
+
function clean(value: unknown): string {
|
|
9
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function cloneConfig(config: Record<string, string> | null | undefined): Record<string, string> {
|
|
13
|
+
return config ? { ...config } : {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getLegacyPrivateKey(config: Record<string, string> | null | undefined): string {
|
|
17
|
+
return clean(config?.privateKey)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stripLegacyPrivateKey(config: Record<string, string> | null | undefined): Record<string, string> {
|
|
21
|
+
const next = cloneConfig(config)
|
|
22
|
+
delete next.privateKey
|
|
23
|
+
return next
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildCredentialName(connectorName: string): string {
|
|
27
|
+
const normalizedName = clean(connectorName)
|
|
28
|
+
return normalizedName ? `${normalizedName} SwarmDock Identity Key` : 'SwarmDock Identity Key'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function persistConnectorSecretMigration(
|
|
32
|
+
connector: Connector,
|
|
33
|
+
credentialId: string,
|
|
34
|
+
): Connector {
|
|
35
|
+
const nextConfig = stripLegacyPrivateKey(connector.config)
|
|
36
|
+
const next: Connector = {
|
|
37
|
+
...connector,
|
|
38
|
+
credentialId,
|
|
39
|
+
config: nextConfig,
|
|
40
|
+
updatedAt: Date.now(),
|
|
41
|
+
}
|
|
42
|
+
upsertConnector(next.id, next)
|
|
43
|
+
notify('connectors')
|
|
44
|
+
return next
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function redactConnectorSecrets<T extends Connector>(connector: T): T {
|
|
48
|
+
if (connector.platform !== 'swarmdock') {
|
|
49
|
+
return {
|
|
50
|
+
...connector,
|
|
51
|
+
config: cloneConfig(connector.config),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
...connector,
|
|
56
|
+
config: stripLegacyPrivateKey(connector.config),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function prepareSwarmdockConnectorInput(params: {
|
|
61
|
+
platform: Connector['platform']
|
|
62
|
+
name: string
|
|
63
|
+
credentialId: string | null
|
|
64
|
+
config: Record<string, string> | null | undefined
|
|
65
|
+
}): {
|
|
66
|
+
credentialId: string | null
|
|
67
|
+
config: Record<string, string>
|
|
68
|
+
} {
|
|
69
|
+
const config = cloneConfig(params.config)
|
|
70
|
+
if (params.platform !== 'swarmdock') {
|
|
71
|
+
return {
|
|
72
|
+
credentialId: clean(params.credentialId) || null,
|
|
73
|
+
config,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const credentialId = clean(params.credentialId)
|
|
78
|
+
const legacyPrivateKey = getLegacyPrivateKey(config)
|
|
79
|
+
if (!legacyPrivateKey) {
|
|
80
|
+
return {
|
|
81
|
+
credentialId: credentialId || null,
|
|
82
|
+
config: stripLegacyPrivateKey(config),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (credentialId) {
|
|
87
|
+
return {
|
|
88
|
+
credentialId,
|
|
89
|
+
config: stripLegacyPrivateKey(config),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const credential = createCredentialRecord({
|
|
94
|
+
provider: SWARMMDOCK_CREDENTIAL_PROVIDER,
|
|
95
|
+
name: buildCredentialName(params.name),
|
|
96
|
+
apiKey: legacyPrivateKey,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
credentialId: credential.id,
|
|
101
|
+
config: stripLegacyPrivateKey(config),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function ensureSwarmdockConnectorCredential(
|
|
106
|
+
connector: Connector,
|
|
107
|
+
options?: { allowMigrationFailureFallback?: boolean },
|
|
108
|
+
): {
|
|
109
|
+
connector: Connector
|
|
110
|
+
fallbackPrivateKey: string | null
|
|
111
|
+
} {
|
|
112
|
+
if (connector.platform !== 'swarmdock') {
|
|
113
|
+
return { connector, fallbackPrivateKey: null }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const legacyPrivateKey = getLegacyPrivateKey(connector.config)
|
|
117
|
+
if (!legacyPrivateKey) {
|
|
118
|
+
return { connector, fallbackPrivateKey: null }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const configuredCredentialId = clean(connector.credentialId)
|
|
122
|
+
if (configuredCredentialId) {
|
|
123
|
+
if (resolveCredentialSecret(configuredCredentialId)) {
|
|
124
|
+
return {
|
|
125
|
+
connector: persistConnectorSecretMigration(connector, configuredCredentialId),
|
|
126
|
+
fallbackPrivateKey: null,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
connector,
|
|
131
|
+
fallbackPrivateKey: legacyPrivateKey,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const credential = createCredentialRecord({
|
|
137
|
+
provider: SWARMMDOCK_CREDENTIAL_PROVIDER,
|
|
138
|
+
name: buildCredentialName(connector.name),
|
|
139
|
+
apiKey: legacyPrivateKey,
|
|
140
|
+
})
|
|
141
|
+
return {
|
|
142
|
+
connector: persistConnectorSecretMigration(connector, credential.id),
|
|
143
|
+
fallbackPrivateKey: null,
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (!options?.allowMigrationFailureFallback) throw error
|
|
147
|
+
return {
|
|
148
|
+
connector,
|
|
149
|
+
fallbackPrivateKey: legacyPrivateKey,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import { loadTasks, saveTasks } from '@/lib/server/tasks/task-repository'
|
|
3
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
4
|
+
import type { BoardTask } from '@/types/task'
|
|
5
|
+
|
|
6
|
+
interface SwarmDockTask {
|
|
7
|
+
id: string
|
|
8
|
+
requesterId: string
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
skillRequirements: string[]
|
|
12
|
+
budgetMax: string
|
|
13
|
+
deadline: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a SwarmClaw BoardTask from a SwarmDock task assignment.
|
|
18
|
+
* Uses `externalSource` to link back to the SwarmDock task (same pattern as GitHub issue import).
|
|
19
|
+
*/
|
|
20
|
+
export async function createBoardTaskFromAssignment(
|
|
21
|
+
task: SwarmDockTask,
|
|
22
|
+
agentId: string,
|
|
23
|
+
connectorId: string,
|
|
24
|
+
apiUrl: string,
|
|
25
|
+
): Promise<string> {
|
|
26
|
+
const tasks = loadTasks() as Record<string, BoardTask>
|
|
27
|
+
const id = genId()
|
|
28
|
+
const now = Date.now()
|
|
29
|
+
|
|
30
|
+
const boardTask: BoardTask = {
|
|
31
|
+
id,
|
|
32
|
+
title: task.title,
|
|
33
|
+
description: task.description,
|
|
34
|
+
status: 'running',
|
|
35
|
+
agentId,
|
|
36
|
+
createdAt: now,
|
|
37
|
+
updatedAt: now,
|
|
38
|
+
startedAt: now,
|
|
39
|
+
lastActivityAt: now,
|
|
40
|
+
sourceType: 'import',
|
|
41
|
+
externalSource: {
|
|
42
|
+
source: 'swarmdock',
|
|
43
|
+
id: task.id,
|
|
44
|
+
state: 'in_progress',
|
|
45
|
+
url: `${apiUrl}/tasks/${task.id}`,
|
|
46
|
+
},
|
|
47
|
+
tags: task.skillRequirements,
|
|
48
|
+
objective: task.description,
|
|
49
|
+
followupConnectorId: connectorId,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (task.deadline) {
|
|
53
|
+
boardTask.dueAt = new Date(task.deadline).getTime()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
tasks[id] = boardTask
|
|
57
|
+
saveTasks(tasks)
|
|
58
|
+
|
|
59
|
+
logActivity({
|
|
60
|
+
entityType: 'task',
|
|
61
|
+
entityId: id,
|
|
62
|
+
action: 'created',
|
|
63
|
+
actor: 'system',
|
|
64
|
+
summary: `SwarmDock task assigned: "${task.title}"`,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return id
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Update a SwarmClaw BoardTask based on a SwarmDock SSE event.
|
|
72
|
+
*/
|
|
73
|
+
export async function updateBoardTaskFromEvent(
|
|
74
|
+
swarmdockTaskId: string,
|
|
75
|
+
eventType: string,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const tasks = loadTasks() as Record<string, BoardTask>
|
|
78
|
+
const boardTask = Object.values(tasks).find(
|
|
79
|
+
(t) => t.externalSource?.source === 'swarmdock' && t.externalSource.id === swarmdockTaskId,
|
|
80
|
+
)
|
|
81
|
+
if (!boardTask) return
|
|
82
|
+
|
|
83
|
+
const now = Date.now()
|
|
84
|
+
|
|
85
|
+
switch (eventType) {
|
|
86
|
+
case 'task.completed':
|
|
87
|
+
boardTask.status = 'completed'
|
|
88
|
+
boardTask.completedAt = now
|
|
89
|
+
boardTask.checkoutRunId = null
|
|
90
|
+
break
|
|
91
|
+
case 'task.submitted':
|
|
92
|
+
// Results submitted, waiting for approval on SwarmDock
|
|
93
|
+
if (boardTask.externalSource) boardTask.externalSource.state = 'review'
|
|
94
|
+
break
|
|
95
|
+
case 'task.cancelled':
|
|
96
|
+
boardTask.status = 'cancelled'
|
|
97
|
+
boardTask.checkoutRunId = null
|
|
98
|
+
break
|
|
99
|
+
case 'task.failed':
|
|
100
|
+
boardTask.status = 'failed'
|
|
101
|
+
boardTask.checkoutRunId = null
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
boardTask.updatedAt = now
|
|
106
|
+
boardTask.lastActivityAt = now
|
|
107
|
+
saveTasks(tasks)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find a SwarmClaw BoardTask ID by its SwarmDock task ID.
|
|
112
|
+
*/
|
|
113
|
+
export function findBoardTaskBySwarmdockId(swarmdockTaskId: string): string | null {
|
|
114
|
+
const tasks = loadTasks() as Record<string, BoardTask>
|
|
115
|
+
const task = Object.values(tasks).find(
|
|
116
|
+
(t) => t.externalSource?.source === 'swarmdock' && t.externalSource.id === swarmdockTaskId,
|
|
117
|
+
)
|
|
118
|
+
return task?.id || null
|
|
119
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { log } from '@/lib/server/logger'
|
|
2
|
+
import { hmrSingleton } from '@/lib/shared-utils'
|
|
3
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
4
|
+
import type { Connector, InboundMessage } from '@/types/connector'
|
|
5
|
+
import type { PlatformConnector, ConnectorInstance } from '@/lib/server/connectors/types'
|
|
6
|
+
import { createBoardTaskFromAssignment, updateBoardTaskFromEvent, findBoardTaskBySwarmdockId } from './swarmdock-tasks'
|
|
7
|
+
import { shouldAutoBid, submitAutoBid } from './swarmdock-bidding'
|
|
8
|
+
import type { TaskSubmitInput } from '@swarmdock/shared'
|
|
9
|
+
|
|
10
|
+
const TAG = 'swarmdock'
|
|
11
|
+
|
|
12
|
+
// SDK types inlined until @swarmdock/sdk is built and linked
|
|
13
|
+
interface SwarmDockTask {
|
|
14
|
+
id: string
|
|
15
|
+
requesterId: string
|
|
16
|
+
assigneeId: string | null
|
|
17
|
+
title: string
|
|
18
|
+
description: string
|
|
19
|
+
skillRequirements: string[]
|
|
20
|
+
budgetMax: string
|
|
21
|
+
status: string
|
|
22
|
+
deadline: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SwarmDockSSEEvent {
|
|
26
|
+
type: string
|
|
27
|
+
data: Record<string, unknown>
|
|
28
|
+
timestamp: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SwarmDockConfig {
|
|
32
|
+
apiUrl: string
|
|
33
|
+
walletAddress: string
|
|
34
|
+
agentDescription: string
|
|
35
|
+
skills: string
|
|
36
|
+
autoDiscover: boolean
|
|
37
|
+
maxBudget: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseConfig(connector: Connector): SwarmDockConfig {
|
|
41
|
+
const c = connector.config || {}
|
|
42
|
+
return {
|
|
43
|
+
apiUrl: c.apiUrl || 'https://api.swarmdock.ai',
|
|
44
|
+
walletAddress: c.walletAddress || '',
|
|
45
|
+
agentDescription: c.agentDescription || connector.name || '',
|
|
46
|
+
skills: c.skills || '',
|
|
47
|
+
autoDiscover: c.autoDiscover === 'true',
|
|
48
|
+
maxBudget: c.maxBudget || '0',
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildTaskPrompt(task: SwarmDockTask): string {
|
|
53
|
+
const lines: string[] = [
|
|
54
|
+
`# SwarmDock Task: ${task.title}`,
|
|
55
|
+
'',
|
|
56
|
+
task.description,
|
|
57
|
+
'',
|
|
58
|
+
`**Required Skills:** ${task.skillRequirements.join(', ')}`,
|
|
59
|
+
`**Budget:** ${formatUsdc(task.budgetMax)}`,
|
|
60
|
+
]
|
|
61
|
+
if (task.deadline) lines.push(`**Deadline:** ${task.deadline}`)
|
|
62
|
+
lines.push('', 'Complete this task and provide your deliverables. Your response will be submitted as the task result on the SwarmDock marketplace.')
|
|
63
|
+
return lines.join('\n')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatUsdc(microUnits: string): string {
|
|
67
|
+
const cents = BigInt(microUnits)
|
|
68
|
+
const dollars = Number(cents) / 1_000_000
|
|
69
|
+
return `$${dollars.toFixed(2)} USDC`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function submitSwarmdockTaskResult(
|
|
73
|
+
client: { tasks: { submit: (taskId: string, input: TaskSubmitInput) => Promise<unknown> } },
|
|
74
|
+
swarmdockTaskId: string,
|
|
75
|
+
text: string,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const payload: TaskSubmitInput = {
|
|
78
|
+
artifacts: [{ type: 'text/markdown', content: text }],
|
|
79
|
+
files: [],
|
|
80
|
+
}
|
|
81
|
+
await client.tasks.submit(swarmdockTaskId, payload)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Runtime state: maps SwarmDock task IDs → SwarmClaw BoardTask IDs */
|
|
85
|
+
const taskIdMap = hmrSingleton('__swarmclaw_swarmdock_task_map__', () => new Map<string, string>())
|
|
86
|
+
|
|
87
|
+
const swarmdock: PlatformConnector = {
|
|
88
|
+
async start(connector, _botToken, onMessage): Promise<ConnectorInstance> {
|
|
89
|
+
const config = parseConfig(connector)
|
|
90
|
+
const connectorId = connector.id
|
|
91
|
+
const agentId = connector.agentId || ''
|
|
92
|
+
const privateKey = _botToken || ''
|
|
93
|
+
|
|
94
|
+
if (!privateKey) throw new Error('SwarmDock connector requires an Ed25519 private key credential')
|
|
95
|
+
if (!config.walletAddress) throw new Error('SwarmDock connector requires a Base L2 wallet address in config')
|
|
96
|
+
|
|
97
|
+
// Dynamic import of the SDK (must be built and linked first)
|
|
98
|
+
let SwarmDockClient: typeof import('@swarmdock/sdk').SwarmDockClient
|
|
99
|
+
try {
|
|
100
|
+
const sdk = await import('@swarmdock/sdk')
|
|
101
|
+
SwarmDockClient = sdk.SwarmDockClient
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error('SwarmDock SDK (@swarmdock/sdk) is not installed. Run: npm install @swarmdock/sdk')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const client = new SwarmDockClient({
|
|
107
|
+
baseUrl: config.apiUrl,
|
|
108
|
+
privateKey,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Register agent on SwarmDock (Ed25519 challenge-response)
|
|
112
|
+
const skillList = config.skills
|
|
113
|
+
.split(',')
|
|
114
|
+
.map((s) => s.trim())
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.map((skillId) => ({
|
|
117
|
+
skillId,
|
|
118
|
+
skillName: skillId.replace(/-/g, ' '),
|
|
119
|
+
description: `${skillId} capability`,
|
|
120
|
+
category: skillId,
|
|
121
|
+
basePrice: '1000000', // $1.00 default
|
|
122
|
+
}))
|
|
123
|
+
|
|
124
|
+
log.info(TAG, `Registering agent "${connector.name}" on SwarmDock at ${config.apiUrl}`)
|
|
125
|
+
const registration = await client.register({
|
|
126
|
+
displayName: connector.name,
|
|
127
|
+
description: config.agentDescription,
|
|
128
|
+
framework: 'swarmclaw',
|
|
129
|
+
walletAddress: config.walletAddress,
|
|
130
|
+
skills: skillList,
|
|
131
|
+
})
|
|
132
|
+
log.info(TAG, `Registered as ${registration.agent.did} (trust level ${registration.agent.trustLevel})`)
|
|
133
|
+
|
|
134
|
+
logActivity({
|
|
135
|
+
entityType: 'connector',
|
|
136
|
+
entityId: connectorId,
|
|
137
|
+
action: 'swarmdock-registered',
|
|
138
|
+
actor: 'system',
|
|
139
|
+
summary: `Agent "${connector.name}" registered on SwarmDock as ${registration.agent.did}`,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Set up SSE event stream
|
|
143
|
+
let alive = true
|
|
144
|
+
|
|
145
|
+
const handleSSEEvent = async (event: SwarmDockSSEEvent) => {
|
|
146
|
+
if (!alive) return
|
|
147
|
+
try {
|
|
148
|
+
switch (event.type) {
|
|
149
|
+
case 'task.created': {
|
|
150
|
+
if (!config.autoDiscover) break
|
|
151
|
+
const task = event.data as unknown as SwarmDockTask
|
|
152
|
+
if (shouldAutoBid(task, config)) {
|
|
153
|
+
await submitAutoBid(client, task.id, config)
|
|
154
|
+
logActivity({
|
|
155
|
+
entityType: 'connector',
|
|
156
|
+
entityId: connectorId,
|
|
157
|
+
action: 'swarmdock-bid',
|
|
158
|
+
actor: 'system',
|
|
159
|
+
summary: `Auto-bid on SwarmDock task: "${task.title}"`,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case 'task.assigned': {
|
|
166
|
+
const task = event.data as unknown as SwarmDockTask
|
|
167
|
+
if (!task.assigneeId) break
|
|
168
|
+
|
|
169
|
+
// Signal work started on SwarmDock
|
|
170
|
+
try { await client.tasks.start(task.id) } catch {}
|
|
171
|
+
|
|
172
|
+
// Create a BoardTask in SwarmClaw
|
|
173
|
+
const boardTaskId = await createBoardTaskFromAssignment(task, agentId, connectorId, config.apiUrl)
|
|
174
|
+
taskIdMap.set(task.id, boardTaskId)
|
|
175
|
+
|
|
176
|
+
// Dispatch as inbound message to the assigned agent
|
|
177
|
+
const inbound: InboundMessage = {
|
|
178
|
+
platform: 'swarmdock',
|
|
179
|
+
channelId: `swarmdock-task:${task.id}`,
|
|
180
|
+
channelName: `SwarmDock: ${task.title}`,
|
|
181
|
+
senderId: task.requesterId,
|
|
182
|
+
senderName: `SwarmDock Requester`,
|
|
183
|
+
text: buildTaskPrompt(task),
|
|
184
|
+
messageId: task.id,
|
|
185
|
+
}
|
|
186
|
+
await onMessage(inbound)
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
case 'task.completed':
|
|
191
|
+
case 'task.cancelled':
|
|
192
|
+
case 'task.failed': {
|
|
193
|
+
const taskId = (event.data as Record<string, string>).taskId
|
|
194
|
+
if (taskId) await updateBoardTaskFromEvent(taskId, event.type)
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case 'payment.released': {
|
|
199
|
+
const data = event.data as Record<string, string>
|
|
200
|
+
logActivity({
|
|
201
|
+
entityType: 'connector',
|
|
202
|
+
entityId: connectorId,
|
|
203
|
+
action: 'swarmdock-payment',
|
|
204
|
+
actor: 'system',
|
|
205
|
+
summary: `Payment received: ${formatUsdc(data.amount || '0')} for task ${data.taskId}`,
|
|
206
|
+
})
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
log.error(TAG, `Error handling SSE event ${event.type}: ${err instanceof Error ? err.message : String(err)}`)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
client.events.subscribe(handleSSEEvent as (event: unknown) => void)
|
|
216
|
+
|
|
217
|
+
// Token refresh heartbeat (23h, token lasts 24h)
|
|
218
|
+
const heartbeatInterval = setInterval(async () => {
|
|
219
|
+
try {
|
|
220
|
+
await client.heartbeat()
|
|
221
|
+
log.debug(TAG, 'SwarmDock token refreshed')
|
|
222
|
+
} catch (err) {
|
|
223
|
+
log.error(TAG, `SwarmDock heartbeat failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
224
|
+
}
|
|
225
|
+
}, 23 * 60 * 60 * 1000)
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
connector,
|
|
229
|
+
|
|
230
|
+
stop: async () => {
|
|
231
|
+
alive = false
|
|
232
|
+
clearInterval(heartbeatInterval)
|
|
233
|
+
client.events.unsubscribe()
|
|
234
|
+
log.info(TAG, 'SwarmDock connector stopped')
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
sendMessage: async (channelId: string, text: string) => {
|
|
238
|
+
// channelId format: "swarmdock-task:{taskId}"
|
|
239
|
+
const swarmdockTaskId = channelId.replace('swarmdock-task:', '')
|
|
240
|
+
if (!swarmdockTaskId) return
|
|
241
|
+
|
|
242
|
+
await submitSwarmdockTaskResult(client, swarmdockTaskId, text)
|
|
243
|
+
log.info(TAG, `Submitted results for SwarmDock task ${swarmdockTaskId}`)
|
|
244
|
+
|
|
245
|
+
if (findBoardTaskBySwarmdockId(swarmdockTaskId)) {
|
|
246
|
+
await updateBoardTaskFromEvent(swarmdockTaskId, 'task.submitted')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { messageId: swarmdockTaskId }
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export default swarmdock
|