@swarmclawai/swarmclaw 0.7.2 → 0.7.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 +81 -22
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +36 -7
- package/src/app/api/agents/route.ts +12 -1
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +18 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/route.ts +16 -0
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +4 -0
- package/src/cli/index.ts +3 -10
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +207 -16
- package/src/components/agents/inspector-panel.tsx +108 -48
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/chat/chat-area.tsx +29 -13
- package/src/components/chat/chat-card.tsx +4 -20
- package/src/components/chat/chat-header.tsx +255 -353
- package/src/components/chat/chat-list.tsx +7 -9
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +3 -1
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/layout/app-layout.tsx +383 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +245 -46
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +250 -61
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +45 -5
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +946 -110
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +59 -1
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +13 -39
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +27 -967
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +17 -6
- package/src/lib/server/orchestrator.ts +2 -2
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +822 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/queue.ts +3 -20
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +70 -32
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery.ts +22 -4
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +237 -24
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +56 -1
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +150 -7
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +86 -23
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +36 -3
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/subagent.ts +193 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +896 -100
- package/src/lib/server/storage.ts +226 -7
- package/src/lib/server/stream-agent-chat.ts +46 -21
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +44 -7
- package/src/lib/server/tool-capability-policy.ts +6 -0
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +7 -0
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +0 -6
- package/src/stores/use-chat-store.ts +31 -2
- package/src/types/index.ts +287 -44
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Agent } from '@/types'
|
|
2
|
+
import { resolveAgentReference, resolveTaskAgentFromDescription } from './task-mention'
|
|
3
|
+
|
|
4
|
+
export const MANAGED_AGENT_REFERENCE_KEYS = [
|
|
5
|
+
'agentId',
|
|
6
|
+
'agent_id',
|
|
7
|
+
'assignedAgentId',
|
|
8
|
+
'assigned_agent_id',
|
|
9
|
+
'assignedToAgentId',
|
|
10
|
+
'assigned_to_agent_id',
|
|
11
|
+
'assigneeId',
|
|
12
|
+
'assignee_id',
|
|
13
|
+
'assignedAgent',
|
|
14
|
+
'assigned_agent',
|
|
15
|
+
'assignedTo',
|
|
16
|
+
'assigned_to',
|
|
17
|
+
'assignee',
|
|
18
|
+
'agent',
|
|
19
|
+
'owner',
|
|
20
|
+
] as const
|
|
21
|
+
|
|
22
|
+
type AssignmentSource = 'explicit' | 'description' | 'fallback' | 'none'
|
|
23
|
+
|
|
24
|
+
export interface ManagedAgentAssignmentResolution {
|
|
25
|
+
agentId: string | null
|
|
26
|
+
explicitReference: string | null
|
|
27
|
+
unresolvedReference: string | null
|
|
28
|
+
source: AssignmentSource
|
|
29
|
+
hadExplicitInput: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function firstNonEmptyString(
|
|
33
|
+
parsed: Record<string, unknown>,
|
|
34
|
+
keys: readonly string[],
|
|
35
|
+
): string | null {
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
const raw = parsed[key]
|
|
38
|
+
if (typeof raw !== 'string') continue
|
|
39
|
+
const trimmed = raw.trim()
|
|
40
|
+
if (trimmed) return trimmed
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function hasManagedAgentAssignmentInput(
|
|
46
|
+
parsed: Record<string, unknown>,
|
|
47
|
+
keys: readonly string[] = MANAGED_AGENT_REFERENCE_KEYS,
|
|
48
|
+
): boolean {
|
|
49
|
+
return keys.some((key) => Object.prototype.hasOwnProperty.call(parsed, key))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveManagedAgentAssignment(
|
|
53
|
+
parsed: Record<string, unknown>,
|
|
54
|
+
agents: Record<string, Agent>,
|
|
55
|
+
fallbackAgentId?: string | null,
|
|
56
|
+
opts?: {
|
|
57
|
+
allowDescription?: boolean
|
|
58
|
+
keys?: readonly string[]
|
|
59
|
+
},
|
|
60
|
+
): ManagedAgentAssignmentResolution {
|
|
61
|
+
const keys = opts?.keys ?? MANAGED_AGENT_REFERENCE_KEYS
|
|
62
|
+
const explicitReference = firstNonEmptyString(parsed, keys)
|
|
63
|
+
const hadExplicitInput = hasManagedAgentAssignmentInput(parsed, keys)
|
|
64
|
+
if (explicitReference) {
|
|
65
|
+
const resolved = resolveAgentReference(explicitReference, agents)
|
|
66
|
+
return {
|
|
67
|
+
agentId: resolved,
|
|
68
|
+
explicitReference,
|
|
69
|
+
unresolvedReference: resolved ? null : explicitReference,
|
|
70
|
+
source: 'explicit',
|
|
71
|
+
hadExplicitInput,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (opts?.allowDescription !== false) {
|
|
76
|
+
const description = typeof parsed.description === 'string' ? parsed.description.trim() : ''
|
|
77
|
+
if (description) {
|
|
78
|
+
const resolvedFromDescription = resolveTaskAgentFromDescription(description, '', agents).trim()
|
|
79
|
+
if (resolvedFromDescription) {
|
|
80
|
+
return {
|
|
81
|
+
agentId: resolvedFromDescription,
|
|
82
|
+
explicitReference: null,
|
|
83
|
+
unresolvedReference: null,
|
|
84
|
+
source: 'description',
|
|
85
|
+
hadExplicitInput,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const fallback = typeof fallbackAgentId === 'string' ? fallbackAgentId.trim() : ''
|
|
92
|
+
if (fallback) {
|
|
93
|
+
return {
|
|
94
|
+
agentId: fallback,
|
|
95
|
+
explicitReference: null,
|
|
96
|
+
unresolvedReference: null,
|
|
97
|
+
source: 'fallback',
|
|
98
|
+
hadExplicitInput,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
agentId: null,
|
|
104
|
+
explicitReference: null,
|
|
105
|
+
unresolvedReference: null,
|
|
106
|
+
source: 'none',
|
|
107
|
+
hadExplicitInput,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function resolveDelegatorAgentId(
|
|
112
|
+
parsed: Record<string, unknown>,
|
|
113
|
+
agents: Record<string, Agent>,
|
|
114
|
+
fallbackAgentId?: string | null,
|
|
115
|
+
): string | null {
|
|
116
|
+
const explicitDelegator = typeof parsed.delegatedByAgentId === 'string'
|
|
117
|
+
? parsed.delegatedByAgentId.trim()
|
|
118
|
+
: ''
|
|
119
|
+
if (explicitDelegator) {
|
|
120
|
+
return resolveAgentReference(explicitDelegator, agents) || explicitDelegator
|
|
121
|
+
}
|
|
122
|
+
const fallback = typeof fallbackAgentId === 'string' ? fallbackAgentId.trim() : ''
|
|
123
|
+
return fallback || null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isDelegationTaskPayload(parsed: Record<string, unknown>): boolean {
|
|
127
|
+
const sourceType = typeof parsed.sourceType === 'string' ? parsed.sourceType.trim().toLowerCase() : ''
|
|
128
|
+
if (sourceType === 'delegation') return true
|
|
129
|
+
if (typeof parsed.delegatedFromTaskId === 'string' && parsed.delegatedFromTaskId.trim()) return true
|
|
130
|
+
if (typeof parsed.delegatedByAgentId === 'string' && parsed.delegatedByAgentId.trim()) return true
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function validateManagedAgentAssignment(params: {
|
|
135
|
+
resourceLabel: string
|
|
136
|
+
agents: Record<string, Agent>
|
|
137
|
+
assignScope: 'self' | 'all'
|
|
138
|
+
currentAgentId?: string | null
|
|
139
|
+
targetAgentId?: string | null
|
|
140
|
+
unresolvedReference?: string | null
|
|
141
|
+
isDelegation?: boolean
|
|
142
|
+
delegatorAgentId?: string | null
|
|
143
|
+
}): string | null {
|
|
144
|
+
const currentAgentId = typeof params.currentAgentId === 'string' ? params.currentAgentId.trim() : ''
|
|
145
|
+
const targetAgentId = typeof params.targetAgentId === 'string' ? params.targetAgentId.trim() : ''
|
|
146
|
+
const unresolvedReference = typeof params.unresolvedReference === 'string' ? params.unresolvedReference.trim() : ''
|
|
147
|
+
const delegatorAgentId = typeof params.delegatorAgentId === 'string' ? params.delegatorAgentId.trim() : ''
|
|
148
|
+
|
|
149
|
+
if (unresolvedReference) {
|
|
150
|
+
return `Error: Unknown agent "${unresolvedReference}". Use an existing agent ID or exact agent name.`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (targetAgentId && !params.agents[targetAgentId]) {
|
|
154
|
+
return `Error: Unknown agent "${targetAgentId}". Use an existing agent ID or exact agent name.`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (params.assignScope === 'self' && currentAgentId && targetAgentId && targetAgentId !== currentAgentId) {
|
|
158
|
+
return `Error: You can only assign ${params.resourceLabel} to yourself ("${currentAgentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (params.isDelegation && targetAgentId) {
|
|
162
|
+
const comparisonId = delegatorAgentId || currentAgentId
|
|
163
|
+
if (comparisonId && targetAgentId === comparisonId) {
|
|
164
|
+
return 'Error: Delegation target must be a different agent ID. Create a normal self-task instead of delegating to yourself.'
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
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 { describe, it } 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-approval-connector-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: tempDir,
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
},
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
})
|
|
22
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
23
|
+
const lines = (result.stdout || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
29
|
+
return JSON.parse(jsonLine || '{}')
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('approval connector reminders', () => {
|
|
36
|
+
it('resolves a due approval to the session connector target and records one-shot delivery state', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
39
|
+
const approvalsMod = await import('./src/lib/server/approvals.ts')
|
|
40
|
+
const storage = storageMod.default || storageMod
|
|
41
|
+
const approvals = approvalsMod.default || approvalsMod
|
|
42
|
+
|
|
43
|
+
const now = Date.now()
|
|
44
|
+
storage.saveSettings({
|
|
45
|
+
approvalConnectorNotifyEnabled: true,
|
|
46
|
+
approvalConnectorNotifyDelaySec: 60,
|
|
47
|
+
})
|
|
48
|
+
storage.saveAgents({
|
|
49
|
+
agent_1: {
|
|
50
|
+
id: 'agent_1',
|
|
51
|
+
name: 'Molly',
|
|
52
|
+
description: 'Test agent',
|
|
53
|
+
systemPrompt: 'You are Molly.',
|
|
54
|
+
provider: 'openai',
|
|
55
|
+
model: 'gpt-test',
|
|
56
|
+
plugins: [],
|
|
57
|
+
createdAt: now,
|
|
58
|
+
updatedAt: now,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
storage.saveSessions({
|
|
62
|
+
session_1: {
|
|
63
|
+
id: 'session_1',
|
|
64
|
+
name: 'Connector session',
|
|
65
|
+
cwd: process.cwd(),
|
|
66
|
+
user: 'tester',
|
|
67
|
+
provider: 'openai',
|
|
68
|
+
model: 'gpt-test',
|
|
69
|
+
credentialId: null,
|
|
70
|
+
apiEndpoint: null,
|
|
71
|
+
claudeSessionId: null,
|
|
72
|
+
codexThreadId: null,
|
|
73
|
+
opencodeSessionId: null,
|
|
74
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
75
|
+
messages: [
|
|
76
|
+
{
|
|
77
|
+
role: 'user',
|
|
78
|
+
text: 'Please ask me before spending money.',
|
|
79
|
+
time: now - 1_000,
|
|
80
|
+
source: { connectorId: 'conn-1', channelId: 'chat-42', threadId: 'topic-7' },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
createdAt: now - 120_000,
|
|
84
|
+
lastActiveAt: now - 1_000,
|
|
85
|
+
sessionType: 'human',
|
|
86
|
+
agentId: 'agent_1',
|
|
87
|
+
plugins: [],
|
|
88
|
+
connectorContext: {
|
|
89
|
+
connectorId: 'conn-1',
|
|
90
|
+
platform: 'telegram',
|
|
91
|
+
channelId: 'chat-42',
|
|
92
|
+
threadId: 'topic-7',
|
|
93
|
+
lastInboundAt: now - 1_000,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const approval = approvals.requestApproval({
|
|
99
|
+
category: 'human_loop',
|
|
100
|
+
title: 'Approve plugin install',
|
|
101
|
+
description: 'Need permission to install a plugin.',
|
|
102
|
+
data: {},
|
|
103
|
+
sessionId: 'session_1',
|
|
104
|
+
agentId: 'agent_1',
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const dueAt = approval.createdAt + 61_000
|
|
108
|
+
const reminders = approvals.listPendingApprovalsNeedingConnectorNotification({
|
|
109
|
+
now: dueAt,
|
|
110
|
+
runningConnectors: [
|
|
111
|
+
{ id: 'conn-1', agentId: 'agent_1', supportsSend: true, configuredTargets: [], recentChannelId: 'chat-42' },
|
|
112
|
+
],
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
approvals.markApprovalConnectorNotificationSent(approval.id, {
|
|
116
|
+
at: dueAt,
|
|
117
|
+
connectorId: 'conn-1',
|
|
118
|
+
channelId: 'chat-42',
|
|
119
|
+
threadId: 'topic-7',
|
|
120
|
+
messageId: 'msg-9',
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const afterSend = approvals.listPendingApprovalsNeedingConnectorNotification({
|
|
124
|
+
now: dueAt + 1_000,
|
|
125
|
+
runningConnectors: [
|
|
126
|
+
{ id: 'conn-1', agentId: 'agent_1', supportsSend: true, configuredTargets: [], recentChannelId: 'chat-42' },
|
|
127
|
+
],
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const storedApproval = storage.loadApprovals()[approval.id]
|
|
131
|
+
console.log(JSON.stringify({
|
|
132
|
+
reminderCount: reminders.length,
|
|
133
|
+
reminder: reminders[0],
|
|
134
|
+
afterSendCount: afterSend.length,
|
|
135
|
+
storedApproval,
|
|
136
|
+
}))
|
|
137
|
+
`)
|
|
138
|
+
|
|
139
|
+
assert.equal(output.reminderCount, 1)
|
|
140
|
+
assert.equal(output.reminder.connectorId, 'conn-1')
|
|
141
|
+
assert.equal(output.reminder.channelId, 'chat-42')
|
|
142
|
+
assert.equal(output.reminder.threadId, 'topic-7')
|
|
143
|
+
assert.match(output.reminder.text, /Molly is waiting for your approval/i)
|
|
144
|
+
assert.equal(output.afterSendCount, 0)
|
|
145
|
+
assert.equal(output.storedApproval.connectorNotification.sentAt > 0, true)
|
|
146
|
+
assert.equal(output.storedApproval.connectorNotification.messageId, 'msg-9')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('falls back to a running owned connector and respects retry cooldowns after failed sends', () => {
|
|
150
|
+
const output = runWithTempDataDir(`
|
|
151
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
152
|
+
const approvalsMod = await import('./src/lib/server/approvals.ts')
|
|
153
|
+
const storage = storageMod.default || storageMod
|
|
154
|
+
const approvals = approvalsMod.default || approvalsMod
|
|
155
|
+
|
|
156
|
+
const now = Date.now()
|
|
157
|
+
storage.saveSettings({
|
|
158
|
+
approvalConnectorNotifyEnabled: true,
|
|
159
|
+
approvalConnectorNotifyDelaySec: 30,
|
|
160
|
+
})
|
|
161
|
+
storage.saveAgents({
|
|
162
|
+
agent_2: {
|
|
163
|
+
id: 'agent_2',
|
|
164
|
+
name: 'Writer',
|
|
165
|
+
description: 'Test agent',
|
|
166
|
+
systemPrompt: 'You are Writer.',
|
|
167
|
+
provider: 'openai',
|
|
168
|
+
model: 'gpt-test',
|
|
169
|
+
plugins: [],
|
|
170
|
+
createdAt: now,
|
|
171
|
+
updatedAt: now,
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
storage.saveSessions({
|
|
175
|
+
session_plain: {
|
|
176
|
+
id: 'session_plain',
|
|
177
|
+
name: 'Non-connector session',
|
|
178
|
+
cwd: process.cwd(),
|
|
179
|
+
user: 'tester',
|
|
180
|
+
provider: 'openai',
|
|
181
|
+
model: 'gpt-test',
|
|
182
|
+
credentialId: null,
|
|
183
|
+
apiEndpoint: null,
|
|
184
|
+
claudeSessionId: null,
|
|
185
|
+
codexThreadId: null,
|
|
186
|
+
opencodeSessionId: null,
|
|
187
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
188
|
+
messages: [],
|
|
189
|
+
createdAt: now - 60_000,
|
|
190
|
+
lastActiveAt: now - 1_000,
|
|
191
|
+
sessionType: 'human',
|
|
192
|
+
agentId: 'agent_2',
|
|
193
|
+
plugins: [],
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const approval = approvals.requestApproval({
|
|
198
|
+
category: 'task_tool',
|
|
199
|
+
title: 'Approve outbound outreach',
|
|
200
|
+
description: 'Need your approval before sending a message.',
|
|
201
|
+
data: {},
|
|
202
|
+
sessionId: 'session_plain',
|
|
203
|
+
agentId: 'agent_2',
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const dueAt = approval.createdAt + 31_000
|
|
207
|
+
const first = approvals.listPendingApprovalsNeedingConnectorNotification({
|
|
208
|
+
now: dueAt,
|
|
209
|
+
runningConnectors: [
|
|
210
|
+
{ id: 'conn-fallback', agentId: 'agent_2', supportsSend: true, configuredTargets: [], recentChannelId: 'dm-88' },
|
|
211
|
+
],
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
approvals.markApprovalConnectorNotificationAttempt(approval.id, {
|
|
215
|
+
at: dueAt,
|
|
216
|
+
connectorId: 'conn-fallback',
|
|
217
|
+
channelId: 'dm-88',
|
|
218
|
+
lastError: 'connector temporarily unavailable',
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const withinCooldown = approvals.listPendingApprovalsNeedingConnectorNotification({
|
|
222
|
+
now: dueAt + 5_000,
|
|
223
|
+
runningConnectors: [
|
|
224
|
+
{ id: 'conn-fallback', agentId: 'agent_2', supportsSend: true, configuredTargets: [], recentChannelId: 'dm-88' },
|
|
225
|
+
],
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const afterCooldown = approvals.listPendingApprovalsNeedingConnectorNotification({
|
|
229
|
+
now: dueAt + (10 * 60_000) + 1_000,
|
|
230
|
+
runningConnectors: [
|
|
231
|
+
{ id: 'conn-fallback', agentId: 'agent_2', supportsSend: true, configuredTargets: [], recentChannelId: 'dm-88' },
|
|
232
|
+
],
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const storedApproval = storage.loadApprovals()[approval.id]
|
|
236
|
+
console.log(JSON.stringify({
|
|
237
|
+
firstCount: first.length,
|
|
238
|
+
fallbackConnectorId: first[0]?.connectorId || null,
|
|
239
|
+
fallbackChannelId: first[0]?.channelId || null,
|
|
240
|
+
withinCooldownCount: withinCooldown.length,
|
|
241
|
+
afterCooldownCount: afterCooldown.length,
|
|
242
|
+
storedApproval,
|
|
243
|
+
}))
|
|
244
|
+
`)
|
|
245
|
+
|
|
246
|
+
assert.equal(output.firstCount, 1)
|
|
247
|
+
assert.equal(output.fallbackConnectorId, 'conn-fallback')
|
|
248
|
+
assert.equal(output.fallbackChannelId, 'dm-88')
|
|
249
|
+
assert.equal(output.withinCooldownCount, 0)
|
|
250
|
+
assert.equal(output.afterCooldownCount, 1)
|
|
251
|
+
assert.equal(output.storedApproval.connectorNotification.lastError, 'connector temporarily unavailable')
|
|
252
|
+
})
|
|
253
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
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 { describe, it } 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-approval-auto-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: tempDir,
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
},
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
})
|
|
22
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
23
|
+
const lines = (result.stdout || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
29
|
+
return JSON.parse(jsonLine || '{}')
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('approval auto-approve', () => {
|
|
36
|
+
it('auto-approves tool access and plugin scaffolds when configured', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
39
|
+
const approvalsMod = await import('./src/lib/server/approvals.ts')
|
|
40
|
+
const dataDirMod = await import('./src/lib/server/data-dir.ts')
|
|
41
|
+
const storage = storageMod.default || storageMod
|
|
42
|
+
const approvals = approvalsMod.default || approvalsMod
|
|
43
|
+
const dataDir = dataDirMod.DATA_DIR || dataDirMod.default?.DATA_DIR || dataDirMod['module.exports']?.DATA_DIR
|
|
44
|
+
|
|
45
|
+
storage.saveSettings({
|
|
46
|
+
approvalAutoApproveCategories: ['tool_access', 'plugin_scaffold'],
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const now = Date.now()
|
|
50
|
+
storage.saveSessions({
|
|
51
|
+
session_auto: {
|
|
52
|
+
id: 'session_auto',
|
|
53
|
+
name: 'Auto Approval Test',
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
user: 'tester',
|
|
56
|
+
provider: 'openai',
|
|
57
|
+
model: 'gpt-test',
|
|
58
|
+
credentialId: null,
|
|
59
|
+
apiEndpoint: null,
|
|
60
|
+
claudeSessionId: null,
|
|
61
|
+
codexThreadId: null,
|
|
62
|
+
opencodeSessionId: null,
|
|
63
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
64
|
+
messages: [],
|
|
65
|
+
createdAt: now,
|
|
66
|
+
lastActiveAt: now,
|
|
67
|
+
sessionType: 'human',
|
|
68
|
+
agentId: 'default',
|
|
69
|
+
plugins: [],
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const toolApproval = await approvals.requestApprovalMaybeAutoApprove({
|
|
74
|
+
category: 'tool_access',
|
|
75
|
+
title: 'Enable Plugin: shell',
|
|
76
|
+
data: { toolId: 'shell', pluginId: 'shell' },
|
|
77
|
+
sessionId: 'session_auto',
|
|
78
|
+
agentId: 'default',
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const pluginApproval = await approvals.requestApprovalMaybeAutoApprove({
|
|
82
|
+
category: 'plugin_scaffold',
|
|
83
|
+
title: 'Scaffold Plugin: auto-test.js',
|
|
84
|
+
data: {
|
|
85
|
+
filename: 'auto-test.js',
|
|
86
|
+
code: 'module.exports = { name: \"AutoTestPlugin\" }',
|
|
87
|
+
createdByAgentId: 'default',
|
|
88
|
+
},
|
|
89
|
+
sessionId: 'session_auto',
|
|
90
|
+
agentId: 'default',
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const sessions = storage.loadSessions()
|
|
94
|
+
const pluginsDir = await import('node:path').then((path) => path.join(dataDir, 'plugins'))
|
|
95
|
+
const pluginPath = await import('node:path').then((path) => path.join(pluginsDir, 'auto-test.js'))
|
|
96
|
+
|
|
97
|
+
console.log(JSON.stringify({
|
|
98
|
+
categories: approvals.listAutoApprovableApprovalCategories(),
|
|
99
|
+
toolApprovalStatus: toolApproval.status,
|
|
100
|
+
pluginApprovalStatus: pluginApproval.status,
|
|
101
|
+
sessionPlugins: sessions.session_auto.plugins,
|
|
102
|
+
pluginExists: (await import('node:fs')).existsSync(pluginPath),
|
|
103
|
+
}))
|
|
104
|
+
`)
|
|
105
|
+
|
|
106
|
+
assert.equal(output.toolApprovalStatus, 'approved')
|
|
107
|
+
assert.equal(output.pluginApprovalStatus, 'approved')
|
|
108
|
+
assert.equal(Array.isArray(output.categories), true)
|
|
109
|
+
assert.equal(output.categories.includes('wallet_transfer'), true)
|
|
110
|
+
assert.equal(output.sessionPlugins.includes('shell'), true)
|
|
111
|
+
assert.equal(output.pluginExists, true)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('can disable approvals platform-wide for fully autonomous execution', () => {
|
|
115
|
+
const output = runWithTempDataDir(`
|
|
116
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
117
|
+
const approvalsMod = await import('./src/lib/server/approvals.ts')
|
|
118
|
+
const storage = storageMod.default || storageMod
|
|
119
|
+
const approvals = approvalsMod.default || approvalsMod
|
|
120
|
+
|
|
121
|
+
storage.saveSettings({
|
|
122
|
+
approvalsEnabled: false,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const approval = await approvals.requestApprovalMaybeAutoApprove({
|
|
126
|
+
category: 'human_loop',
|
|
127
|
+
title: 'Need an answer',
|
|
128
|
+
description: 'Should be auto-approved because approvals are disabled platform-wide.',
|
|
129
|
+
data: { question: 'Proceed?' },
|
|
130
|
+
agentId: 'default',
|
|
131
|
+
sessionId: null,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const stored = storage.loadApprovals()[approval.id]
|
|
135
|
+
console.log(JSON.stringify({
|
|
136
|
+
approvalStatus: approval.status,
|
|
137
|
+
storedStatus: stored?.status || null,
|
|
138
|
+
}))
|
|
139
|
+
`)
|
|
140
|
+
|
|
141
|
+
assert.equal(output.approvalStatus, 'approved')
|
|
142
|
+
assert.equal(output.storedStatus, 'approved')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('adds a pending approval request message to the chat session when approvals are enabled', () => {
|
|
146
|
+
const output = runWithTempDataDir(`
|
|
147
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
148
|
+
const approvalsMod = await import('./src/lib/server/approvals.ts')
|
|
149
|
+
const storage = storageMod.default || storageMod
|
|
150
|
+
const approvals = approvalsMod.default || approvalsMod
|
|
151
|
+
|
|
152
|
+
const now = Date.now()
|
|
153
|
+
storage.saveSettings({
|
|
154
|
+
approvalsEnabled: true,
|
|
155
|
+
approvalAutoApproveCategories: [],
|
|
156
|
+
})
|
|
157
|
+
storage.saveSessions({
|
|
158
|
+
session_chat: {
|
|
159
|
+
id: 'session_chat',
|
|
160
|
+
name: 'Approval Chat Test',
|
|
161
|
+
cwd: process.cwd(),
|
|
162
|
+
user: 'tester',
|
|
163
|
+
provider: 'openai',
|
|
164
|
+
model: 'gpt-test',
|
|
165
|
+
credentialId: null,
|
|
166
|
+
apiEndpoint: null,
|
|
167
|
+
claudeSessionId: null,
|
|
168
|
+
codexThreadId: null,
|
|
169
|
+
opencodeSessionId: null,
|
|
170
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
171
|
+
messages: [],
|
|
172
|
+
createdAt: now,
|
|
173
|
+
lastActiveAt: now,
|
|
174
|
+
sessionType: 'human',
|
|
175
|
+
agentId: 'default',
|
|
176
|
+
plugins: [],
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const approval = await approvals.requestApprovalMaybeAutoApprove({
|
|
181
|
+
category: 'tool_access',
|
|
182
|
+
title: 'Enable Plugin: shell',
|
|
183
|
+
description: 'Need shell access for a task.',
|
|
184
|
+
data: { toolId: 'shell', pluginId: 'shell' },
|
|
185
|
+
sessionId: 'session_chat',
|
|
186
|
+
agentId: 'default',
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const session = storage.loadSessions().session_chat
|
|
190
|
+
const lastMessage = session.messages.at(-1)
|
|
191
|
+
console.log(JSON.stringify({
|
|
192
|
+
approvalStatus: approval.status,
|
|
193
|
+
messageCount: session.messages.length,
|
|
194
|
+
lastMessage,
|
|
195
|
+
}))
|
|
196
|
+
`)
|
|
197
|
+
|
|
198
|
+
assert.equal(output.approvalStatus, 'pending')
|
|
199
|
+
assert.equal(output.messageCount, 1)
|
|
200
|
+
assert.equal(output.lastMessage.role, 'assistant')
|
|
201
|
+
assert.equal(output.lastMessage.kind, 'system')
|
|
202
|
+
assert.match(output.lastMessage.text, /\"type\":\"plugin_request\"/)
|
|
203
|
+
assert.match(output.lastMessage.text, /\"pluginId\":\"shell\"/)
|
|
204
|
+
})
|
|
205
|
+
})
|