@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
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
-
import { spawn, spawnSync } from 'child_process'
|
|
3
|
+
import { spawn, spawnSync, type ChildProcess } from 'child_process'
|
|
4
4
|
import type { ToolBuildContext } from './context'
|
|
5
5
|
import { truncate, findBinaryOnPath, MAX_OUTPUT } from './context'
|
|
6
6
|
import type { Plugin, PluginHooks } from '@/types'
|
|
7
7
|
import { getPluginManager } from '../plugins'
|
|
8
8
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
9
|
+
import {
|
|
10
|
+
appendDelegationCheckpoint,
|
|
11
|
+
cancelDelegationJob,
|
|
12
|
+
completeDelegationJob,
|
|
13
|
+
createDelegationJob,
|
|
14
|
+
failDelegationJob,
|
|
15
|
+
getDelegationJob,
|
|
16
|
+
listDelegationJobs,
|
|
17
|
+
recoverStaleDelegationJobs,
|
|
18
|
+
registerDelegationRuntime,
|
|
19
|
+
startDelegationJob,
|
|
20
|
+
} from '../delegation-jobs'
|
|
21
|
+
import { markProviderFailure, markProviderSuccess } from '../provider-health'
|
|
9
22
|
|
|
10
23
|
const MAX_DELEGATION_CHAIN_HOPS = 128
|
|
24
|
+
const DELEGATE_BACKEND_ORDER: DelegateBackend[] = ['claude', 'codex', 'opencode', 'gemini']
|
|
11
25
|
|
|
12
26
|
interface DelegateContext {
|
|
27
|
+
id?: string
|
|
28
|
+
sessionId?: string | null
|
|
29
|
+
agentId?: string | null
|
|
30
|
+
jobId?: string | null
|
|
13
31
|
cwd?: string
|
|
14
32
|
claudeTimeoutMs?: number
|
|
15
33
|
readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null
|
|
16
|
-
persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', id: string) => void
|
|
17
|
-
ctx?: { platformAssignScope?: string; agentId?: string | null }
|
|
34
|
+
persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', id: string | null | undefined) => void
|
|
35
|
+
ctx?: { platformAssignScope?: string; agentId?: string | null; sessionId?: string | null }
|
|
18
36
|
hasPlugin?: (name: string) => boolean
|
|
19
37
|
/** @deprecated Use hasPlugin */
|
|
20
38
|
hasTool?: (name: string) => boolean
|
|
@@ -22,6 +40,11 @@ interface DelegateContext {
|
|
|
22
40
|
|
|
23
41
|
type DelegateBackend = 'claude' | 'codex' | 'opencode' | 'gemini'
|
|
24
42
|
|
|
43
|
+
interface DelegateRuntimeState {
|
|
44
|
+
child?: ChildProcess | null
|
|
45
|
+
cancel?: () => void
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
function asTaskRecord(value: unknown): Record<string, unknown> | null {
|
|
26
49
|
return value && typeof value === 'object' ? value as Record<string, unknown> : null
|
|
27
50
|
}
|
|
@@ -63,11 +86,169 @@ function _computeDelegationDepth(
|
|
|
63
86
|
return depth
|
|
64
87
|
}
|
|
65
88
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
89
|
+
function sleep(ms: number) {
|
|
90
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildDelegateContextFromSessionish(session: unknown): DelegateContext {
|
|
94
|
+
const record = session && typeof session === 'object' ? session as Record<string, unknown> : {}
|
|
95
|
+
const sessionId = typeof record.id === 'string'
|
|
96
|
+
? record.id
|
|
97
|
+
: typeof record.sessionId === 'string'
|
|
98
|
+
? record.sessionId
|
|
99
|
+
: null
|
|
100
|
+
const agentId = typeof record.agentId === 'string' ? record.agentId : null
|
|
101
|
+
const platformAssignScope = typeof record.platformAssignScope === 'string' ? record.platformAssignScope : undefined
|
|
102
|
+
const storedResumeIds = record.delegateResumeIds && typeof record.delegateResumeIds === 'object'
|
|
103
|
+
? record.delegateResumeIds as Record<string, unknown>
|
|
104
|
+
: null
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
cwd: typeof record.cwd === 'string' ? record.cwd : process.cwd(),
|
|
108
|
+
claudeTimeoutMs: typeof record.claudeTimeoutMs === 'number' ? record.claudeTimeoutMs : undefined,
|
|
109
|
+
readStoredDelegateResumeId: typeof record.readStoredDelegateResumeId === 'function'
|
|
110
|
+
? record.readStoredDelegateResumeId as DelegateContext['readStoredDelegateResumeId']
|
|
111
|
+
: (key) => {
|
|
112
|
+
const raw = storedResumeIds?.[key]
|
|
113
|
+
return typeof raw === 'string' && raw.trim() ? raw.trim() : null
|
|
114
|
+
},
|
|
115
|
+
persistDelegateResumeId: typeof record.persistDelegateResumeId === 'function'
|
|
116
|
+
? record.persistDelegateResumeId as DelegateContext['persistDelegateResumeId']
|
|
117
|
+
: undefined,
|
|
118
|
+
id: typeof record.id === 'string' ? record.id : undefined,
|
|
119
|
+
sessionId,
|
|
120
|
+
agentId,
|
|
121
|
+
ctx: {
|
|
122
|
+
sessionId,
|
|
123
|
+
agentId,
|
|
124
|
+
platformAssignScope,
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildDelegateResumePatch(bctx: DelegateContext) {
|
|
130
|
+
const resumeIds = {
|
|
131
|
+
claudeCode: bctx.readStoredDelegateResumeId?.('claudeCode') || null,
|
|
132
|
+
codex: bctx.readStoredDelegateResumeId?.('codex') || null,
|
|
133
|
+
opencode: bctx.readStoredDelegateResumeId?.('opencode') || null,
|
|
134
|
+
gemini: bctx.readStoredDelegateResumeId?.('gemini') || null,
|
|
135
|
+
}
|
|
136
|
+
const resumeId = resumeIds.claudeCode || resumeIds.codex || resumeIds.opencode || resumeIds.gemini || null
|
|
137
|
+
return { resumeIds, resumeId }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function coerceDelegateBackend(value: unknown): DelegateBackend | null {
|
|
141
|
+
const normalized = String(value || '').trim().toLowerCase()
|
|
142
|
+
if (!normalized) return null
|
|
143
|
+
if (['claude', 'claude code', 'claude-code', 'claude_code'].includes(normalized)) return 'claude'
|
|
144
|
+
if (['codex', 'codex cli', 'codex-cli', 'codex_cli'].includes(normalized)) return 'codex'
|
|
145
|
+
if (['opencode', 'open code', 'open-code', 'open_code'].includes(normalized)) return 'opencode'
|
|
146
|
+
if (['gemini', 'gemini cli', 'gemini-cli', 'gemini_cli'].includes(normalized)) return 'gemini'
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildDelegateTaskFromPayload(normalized: Record<string, unknown>): string | null {
|
|
151
|
+
const action = String(normalized.action || '').trim().toLowerCase()
|
|
152
|
+
const target = [
|
|
153
|
+
normalized.target,
|
|
154
|
+
normalized.path,
|
|
155
|
+
normalized.filePath,
|
|
156
|
+
normalized.filename,
|
|
157
|
+
normalized.name,
|
|
158
|
+
].find((value) => typeof value === 'string' && value.trim()) as string | undefined
|
|
159
|
+
const content = typeof normalized.content === 'string' ? normalized.content.trim() : ''
|
|
160
|
+
const taskName = typeof normalized.name === 'string' ? normalized.name.trim() : ''
|
|
161
|
+
const files = Array.isArray(normalized.files) ? normalized.files : []
|
|
162
|
+
const fileInstructions = files
|
|
163
|
+
.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object' && !Array.isArray(entry))
|
|
164
|
+
.map((entry) => {
|
|
165
|
+
const filePath = typeof entry.path === 'string'
|
|
166
|
+
? entry.path.trim()
|
|
167
|
+
: typeof entry.filePath === 'string'
|
|
168
|
+
? entry.filePath.trim()
|
|
169
|
+
: typeof entry.filename === 'string'
|
|
170
|
+
? entry.filename.trim()
|
|
171
|
+
: ''
|
|
172
|
+
const fileContent = typeof entry.content === 'string' ? entry.content.trim() : ''
|
|
173
|
+
if (!filePath && !fileContent) return ''
|
|
174
|
+
if (filePath && fileContent) {
|
|
175
|
+
return `Create or update "${filePath}" with this content:\n\n${fileContent}`
|
|
176
|
+
}
|
|
177
|
+
if (filePath) return `Create or update "${filePath}".`
|
|
178
|
+
return `Create or update a file with this content:\n\n${fileContent}`
|
|
179
|
+
})
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
|
|
182
|
+
if (['write', 'create', 'create_file', 'create-file', 'createfile'].includes(action)) {
|
|
183
|
+
if (target && content) return `Create or overwrite the file "${target}" with this content:\n\n${content}`
|
|
184
|
+
if (target) return `Create the file "${target}".`
|
|
185
|
+
}
|
|
186
|
+
if (['edit', 'update', 'modify'].includes(action)) {
|
|
187
|
+
if (target && content) return `Update the file "${target}" with this content:\n\n${content}`
|
|
188
|
+
if (target) return `Update the file "${target}".`
|
|
189
|
+
}
|
|
190
|
+
if (target && content) return `Perform the "${action || 'requested'}" task against "${target}" using this content:\n\n${content}`
|
|
191
|
+
if (target) return `Perform the "${action || 'requested'}" task against "${target}".`
|
|
192
|
+
if (fileInstructions.length > 0) {
|
|
193
|
+
const intro = taskName || 'Perform the delegated file task.'
|
|
194
|
+
return `${intro}\n\n${fileInstructions.join('\n\n')}`
|
|
195
|
+
}
|
|
196
|
+
if (content) return `Perform the delegated task with this content:\n\n${content}`
|
|
197
|
+
if (taskName) return taskName
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeDelegateArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
|
|
202
|
+
const normalized = normalizeToolInputArgs(rawArgs)
|
|
203
|
+
const backend = coerceDelegateBackend(
|
|
204
|
+
normalized.backend
|
|
205
|
+
?? normalized.tool_name
|
|
206
|
+
?? normalized.toolName
|
|
207
|
+
?? normalized.delegate
|
|
208
|
+
?? normalized.provider,
|
|
209
|
+
)
|
|
210
|
+
if (backend && !normalized.backend) normalized.backend = backend
|
|
211
|
+
if (typeof normalized.task !== 'string' && typeof normalized.prompt === 'string') normalized.task = normalized.prompt
|
|
212
|
+
const action = String(normalized.action || '').trim().toLowerCase()
|
|
213
|
+
const isLifecycleAction = ['status', 'list', 'wait', 'cancel'].includes(action)
|
|
214
|
+
if (!isLifecycleAction) {
|
|
215
|
+
if (typeof normalized.task !== 'string' || !normalized.task.trim()) {
|
|
216
|
+
const synthesized = buildDelegateTaskFromPayload(normalized)
|
|
217
|
+
if (synthesized) normalized.task = synthesized
|
|
218
|
+
}
|
|
219
|
+
normalized.action = 'start'
|
|
220
|
+
}
|
|
221
|
+
return normalized
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function resolveDelegateSessionId(bctx: DelegateContext): string | null {
|
|
225
|
+
const nested = typeof bctx.ctx?.sessionId === 'string' ? bctx.ctx.sessionId.trim() : ''
|
|
226
|
+
if (nested) return nested
|
|
227
|
+
const direct = typeof bctx.sessionId === 'string' ? bctx.sessionId.trim() : ''
|
|
228
|
+
if (direct) return direct
|
|
229
|
+
const legacy = typeof bctx.id === 'string' ? bctx.id.trim() : ''
|
|
230
|
+
return legacy || null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function bindDelegateRuntime(runtime: DelegateRuntimeState | undefined, child: ChildProcess) {
|
|
234
|
+
if (!runtime) return
|
|
235
|
+
runtime.child = child
|
|
236
|
+
runtime.cancel = () => {
|
|
237
|
+
try {
|
|
238
|
+
child.kill('SIGTERM')
|
|
239
|
+
} catch {
|
|
240
|
+
// best-effort cancel
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const clear = () => {
|
|
244
|
+
if (runtime.child === child) runtime.child = null
|
|
245
|
+
}
|
|
246
|
+
child.once('close', clear)
|
|
247
|
+
child.once('error', clear)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function runDelegateBackend(args: Record<string, unknown>, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
|
|
251
|
+
const normalized = normalizeDelegateArgs(args)
|
|
71
252
|
const task = normalized.task as string
|
|
72
253
|
const backend = ((normalized.backend as string) || 'claude') as DelegateBackend
|
|
73
254
|
const resume = normalized.resume as boolean
|
|
@@ -81,13 +262,209 @@ async function executeDelegateAction(args: Record<string, unknown>, bctx: Delega
|
|
|
81
262
|
const binary = backends[backend as keyof typeof backends]
|
|
82
263
|
if (!binary) return `Error: Backend "${backend}" unavailable.`
|
|
83
264
|
|
|
84
|
-
if (backend === 'claude') return runClaudeDelegate(binary, task, resume, resumeId, bctx)
|
|
85
|
-
if (backend === 'codex') return runCodexDelegate(binary, task, resume, resumeId, bctx)
|
|
86
|
-
if (backend === 'opencode') return runOpenCodeDelegate(binary, task, resume, resumeId, bctx)
|
|
87
|
-
if (backend === 'gemini') return runGeminiDelegate(binary, task, resume, resumeId, bctx)
|
|
265
|
+
if (backend === 'claude') return runClaudeDelegate(binary, task, resume, resumeId, bctx, runtime)
|
|
266
|
+
if (backend === 'codex') return runCodexDelegate(binary, task, resume, resumeId, bctx, runtime)
|
|
267
|
+
if (backend === 'opencode') return runOpenCodeDelegate(binary, task, resume, resumeId, bctx, runtime)
|
|
268
|
+
if (backend === 'gemini') return runGeminiDelegate(binary, task, resume, resumeId, bctx, runtime)
|
|
88
269
|
return `Error: Unsupported backend "${backend}".`
|
|
89
270
|
}
|
|
90
271
|
|
|
272
|
+
function providerIdForBackend(backend: DelegateBackend): string {
|
|
273
|
+
if (backend === 'claude') return 'claude-cli'
|
|
274
|
+
if (backend === 'codex') return 'codex-cli'
|
|
275
|
+
if (backend === 'opencode') return 'opencode-cli'
|
|
276
|
+
return 'gemini-cli'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function fallbackOrderForBackend(requested: DelegateBackend): DelegateBackend[] {
|
|
280
|
+
return [requested, ...DELEGATE_BACKEND_ORDER.filter((backend) => backend !== requested)]
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isRecoverableDelegateFailure(result: string): boolean {
|
|
284
|
+
const normalized = String(result || '').trim().toLowerCase()
|
|
285
|
+
if (!normalized.startsWith('error:')) return false
|
|
286
|
+
return [
|
|
287
|
+
'not authenticated',
|
|
288
|
+
'backend "',
|
|
289
|
+
'unavailable',
|
|
290
|
+
'enoent',
|
|
291
|
+
'not found',
|
|
292
|
+
'command not found',
|
|
293
|
+
'spawn ',
|
|
294
|
+
'eacces',
|
|
295
|
+
'permission denied',
|
|
296
|
+
].some((needle) => normalized.includes(needle))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function summarizeDelegateAttempts(
|
|
300
|
+
requested: DelegateBackend,
|
|
301
|
+
attempts: Array<{ backend: DelegateBackend; result: string }>,
|
|
302
|
+
): string {
|
|
303
|
+
const summary = attempts
|
|
304
|
+
.map(({ backend, result }) => `${backend}: ${result.replace(/^Error:\s*/i, '').trim() || result.trim()}`)
|
|
305
|
+
.join(' | ')
|
|
306
|
+
return `Error: Delegate backend "${requested}" could not complete the task. ${summary}. Continue with another available tool instead of stopping.`
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function runDelegateBackendWithFallback(
|
|
310
|
+
args: Record<string, unknown>,
|
|
311
|
+
bctx: DelegateContext,
|
|
312
|
+
runtime?: DelegateRuntimeState,
|
|
313
|
+
opts?: { onAttempt?: (backend: DelegateBackend, attemptIndex: number) => void; onFallback?: (from: DelegateBackend, to: DelegateBackend, reason: string) => void },
|
|
314
|
+
): Promise<{ backend: DelegateBackend; result: string; attempts: Array<{ backend: DelegateBackend; result: string }> }> {
|
|
315
|
+
const normalized = normalizeDelegateArgs(args)
|
|
316
|
+
const requested = ((normalized.backend as string) || 'claude') as DelegateBackend
|
|
317
|
+
const orderedBackends = fallbackOrderForBackend(requested)
|
|
318
|
+
const attempts: Array<{ backend: DelegateBackend; result: string }> = []
|
|
319
|
+
|
|
320
|
+
for (const [index, backend] of orderedBackends.entries()) {
|
|
321
|
+
opts?.onAttempt?.(backend, index)
|
|
322
|
+
const result = await runDelegateBackend({ ...normalized, backend }, bctx, runtime)
|
|
323
|
+
attempts.push({ backend, result })
|
|
324
|
+
if (/^Error:/i.test(result.trim())) {
|
|
325
|
+
markProviderFailure(providerIdForBackend(backend), result)
|
|
326
|
+
} else {
|
|
327
|
+
markProviderSuccess(providerIdForBackend(backend))
|
|
328
|
+
return { backend, result, attempts }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const nextBackend = orderedBackends[index + 1]
|
|
332
|
+
if (nextBackend && isRecoverableDelegateFailure(result)) {
|
|
333
|
+
opts?.onFallback?.(backend, nextBackend, result)
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
backend,
|
|
338
|
+
result: attempts.length > 1 ? summarizeDelegateAttempts(requested, attempts) : result,
|
|
339
|
+
attempts,
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
backend: requested,
|
|
345
|
+
result: summarizeDelegateAttempts(requested, attempts),
|
|
346
|
+
attempts,
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function waitForDelegateJob(jobId: string, timeoutSec = 30): Promise<string> {
|
|
351
|
+
const timeoutAt = Date.now() + Math.max(1, timeoutSec) * 1000
|
|
352
|
+
while (Date.now() < timeoutAt) {
|
|
353
|
+
const job = getDelegationJob(jobId)
|
|
354
|
+
if (!job) return `Error: delegation job "${jobId}" not found.`
|
|
355
|
+
if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
|
|
356
|
+
return JSON.stringify(job)
|
|
357
|
+
}
|
|
358
|
+
await sleep(1000)
|
|
359
|
+
}
|
|
360
|
+
const latest = getDelegationJob(jobId)
|
|
361
|
+
return latest ? JSON.stringify(latest) : `Error: delegation job "${jobId}" not found.`
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Core Delegate Execution Logic
|
|
366
|
+
*/
|
|
367
|
+
async function executeDelegateAction(args: Record<string, unknown>, bctx: DelegateContext) {
|
|
368
|
+
const normalized = normalizeDelegateArgs(args)
|
|
369
|
+
const action = String(normalized.action || '').trim().toLowerCase()
|
|
370
|
+
const task = normalized.task as string
|
|
371
|
+
const requestedBackend = ((normalized.backend as string) || 'claude') as DelegateBackend
|
|
372
|
+
const jobId = typeof normalized.jobId === 'string' ? normalized.jobId.trim() : ''
|
|
373
|
+
const waitForCompletion = normalized.waitForCompletion !== false && normalized.background !== true
|
|
374
|
+
const parentSessionId = resolveDelegateSessionId(bctx)
|
|
375
|
+
|
|
376
|
+
recoverStaleDelegationJobs()
|
|
377
|
+
|
|
378
|
+
if (action === 'status') {
|
|
379
|
+
if (!jobId) return 'Error: jobId is required.'
|
|
380
|
+
const job = getDelegationJob(jobId)
|
|
381
|
+
return job ? JSON.stringify(job) : `Error: delegation job "${jobId}" not found.`
|
|
382
|
+
}
|
|
383
|
+
if (action === 'list') {
|
|
384
|
+
const jobs = listDelegationJobs({ parentSessionId: parentSessionId || null })
|
|
385
|
+
.filter((job) => job.kind === 'delegate')
|
|
386
|
+
return JSON.stringify(jobs)
|
|
387
|
+
}
|
|
388
|
+
if (action === 'cancel') {
|
|
389
|
+
if (!jobId) return 'Error: jobId is required.'
|
|
390
|
+
const job = cancelDelegationJob(jobId)
|
|
391
|
+
return job ? JSON.stringify(job) : `Error: delegation job "${jobId}" not found.`
|
|
392
|
+
}
|
|
393
|
+
if (action === 'wait') {
|
|
394
|
+
if (!jobId) return 'Error: jobId is required.'
|
|
395
|
+
const timeoutSec = typeof normalized.timeoutSec === 'number' ? normalized.timeoutSec : 30
|
|
396
|
+
return waitForDelegateJob(jobId, timeoutSec)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!task) return 'Error: task is required.'
|
|
400
|
+
|
|
401
|
+
const job = createDelegationJob({
|
|
402
|
+
kind: 'delegate',
|
|
403
|
+
parentSessionId,
|
|
404
|
+
backend: requestedBackend,
|
|
405
|
+
task,
|
|
406
|
+
cwd: bctx.cwd || null,
|
|
407
|
+
})
|
|
408
|
+
appendDelegationCheckpoint(job.id, `Dispatching to ${requestedBackend}`, 'queued')
|
|
409
|
+
startDelegationJob(job.id, { backend: requestedBackend, cwd: bctx.cwd || null })
|
|
410
|
+
const runtimeHandle: DelegateRuntimeState = {}
|
|
411
|
+
registerDelegationRuntime(job.id, runtimeHandle)
|
|
412
|
+
|
|
413
|
+
const runner = runDelegateBackendWithFallback(args, bctx, runtimeHandle, {
|
|
414
|
+
onAttempt: (backend, index) => {
|
|
415
|
+
if (index === 0) return
|
|
416
|
+
appendDelegationCheckpoint(job.id, `Retrying delegate with ${backend}`, 'running')
|
|
417
|
+
startDelegationJob(job.id, { backend, cwd: bctx.cwd || null })
|
|
418
|
+
},
|
|
419
|
+
onFallback: (from, to, reason) => {
|
|
420
|
+
appendDelegationCheckpoint(
|
|
421
|
+
job.id,
|
|
422
|
+
`Delegate ${from} failed: ${reason.replace(/^Error:\s*/i, '').trim()}. Falling back to ${to}.`,
|
|
423
|
+
'running',
|
|
424
|
+
)
|
|
425
|
+
},
|
|
426
|
+
})
|
|
427
|
+
.then(({ backend, result }) => {
|
|
428
|
+
const latest = getDelegationJob(job.id)
|
|
429
|
+
if (latest?.status === 'cancelled') return { backend, result }
|
|
430
|
+
const resumePatch = buildDelegateResumePatch(bctx)
|
|
431
|
+
if (/^Error:/i.test(result.trim())) {
|
|
432
|
+
appendDelegationCheckpoint(job.id, `Delegate failed on ${backend}`, 'failed')
|
|
433
|
+
failDelegationJob(job.id, result.replace(/^Error:\s*/i, '').trim() || result, { ...resumePatch, backend })
|
|
434
|
+
} else {
|
|
435
|
+
appendDelegationCheckpoint(job.id, `Delegate completed on ${backend}`, 'completed')
|
|
436
|
+
completeDelegationJob(job.id, result, { ...resumePatch, backend })
|
|
437
|
+
}
|
|
438
|
+
return { backend, result }
|
|
439
|
+
})
|
|
440
|
+
.catch((err: unknown) => {
|
|
441
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
442
|
+
const latest = getDelegationJob(job.id)
|
|
443
|
+
if (latest?.status === 'cancelled') return { backend: requestedBackend, result: `Error: ${message}` }
|
|
444
|
+
appendDelegationCheckpoint(job.id, `Delegate crashed on ${requestedBackend}: ${message}`, 'failed')
|
|
445
|
+
failDelegationJob(job.id, message, { ...buildDelegateResumePatch(bctx), backend: requestedBackend })
|
|
446
|
+
return { backend: requestedBackend, result: `Error: ${message}` }
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
if (!waitForCompletion) {
|
|
450
|
+
void runner
|
|
451
|
+
return JSON.stringify({
|
|
452
|
+
jobId: job.id,
|
|
453
|
+
status: 'running',
|
|
454
|
+
backend: requestedBackend,
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const { backend, result } = await runner
|
|
459
|
+
const latest = getDelegationJob(job.id)
|
|
460
|
+
return JSON.stringify({
|
|
461
|
+
jobId: job.id,
|
|
462
|
+
status: latest?.status || (/^Error:/i.test(result.trim()) ? 'failed' : 'completed'),
|
|
463
|
+
backend: latest?.backend || backend,
|
|
464
|
+
response: result,
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
91
468
|
function stripEnvPrefixes(input: NodeJS.ProcessEnv, prefixes: string[]): NodeJS.ProcessEnv {
|
|
92
469
|
const out: NodeJS.ProcessEnv = { ...input }
|
|
93
470
|
for (const key of Object.keys(out)) {
|
|
@@ -120,7 +497,7 @@ function parseCodexOutputText(ev: Record<string, unknown>): string | null {
|
|
|
120
497
|
return null
|
|
121
498
|
}
|
|
122
499
|
|
|
123
|
-
async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
|
|
500
|
+
async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
|
|
124
501
|
try {
|
|
125
502
|
const env = stripEnvPrefixes({ ...process.env, TERM: 'dumb', NO_COLOR: '1' }, ['CODEX'])
|
|
126
503
|
const authProbe = spawnSync(binary, ['login', 'status'], { cwd: bctx.cwd, env, encoding: 'utf-8', timeout: 8000 })
|
|
@@ -139,6 +516,7 @@ async function runCodexDelegate(binary: string, task: string, resume: boolean, r
|
|
|
139
516
|
args.push('--json', '--full-auto', '--skip-git-repo-check', '-')
|
|
140
517
|
|
|
141
518
|
const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
519
|
+
bindDelegateRuntime(runtime, child)
|
|
142
520
|
let stdoutBuf = ''
|
|
143
521
|
let stderrBuf = ''
|
|
144
522
|
let responseText = ''
|
|
@@ -201,7 +579,7 @@ async function runCodexDelegate(binary: string, task: string, resume: boolean, r
|
|
|
201
579
|
}
|
|
202
580
|
}
|
|
203
581
|
|
|
204
|
-
async function runOpenCodeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
|
|
582
|
+
async function runOpenCodeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
|
|
205
583
|
try {
|
|
206
584
|
const env = { ...process.env, TERM: 'dumb', NO_COLOR: '1' } as NodeJS.ProcessEnv
|
|
207
585
|
const storedResumeId = bctx.readStoredDelegateResumeId?.('opencode')
|
|
@@ -212,6 +590,7 @@ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean
|
|
|
212
590
|
if (resumeIdToUse) args.push('--session', resumeIdToUse)
|
|
213
591
|
|
|
214
592
|
const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
593
|
+
bindDelegateRuntime(runtime, child)
|
|
215
594
|
let stdoutBuf = ''
|
|
216
595
|
let stderrBuf = ''
|
|
217
596
|
let responseText = ''
|
|
@@ -277,7 +656,7 @@ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean
|
|
|
277
656
|
}
|
|
278
657
|
}
|
|
279
658
|
|
|
280
|
-
async function runGeminiDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
|
|
659
|
+
async function runGeminiDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
|
|
281
660
|
try {
|
|
282
661
|
const env = { ...process.env, TERM: 'dumb', NO_COLOR: '1' } as NodeJS.ProcessEnv
|
|
283
662
|
const storedResumeId = bctx.readStoredDelegateResumeId?.('gemini')
|
|
@@ -288,6 +667,7 @@ async function runGeminiDelegate(binary: string, task: string, resume: boolean,
|
|
|
288
667
|
if (resumeIdToUse) args.push('--resume', resumeIdToUse)
|
|
289
668
|
|
|
290
669
|
const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
670
|
+
bindDelegateRuntime(runtime, child)
|
|
291
671
|
let stdoutBuf = ''
|
|
292
672
|
let stderrBuf = ''
|
|
293
673
|
let responseText = ''
|
|
@@ -357,7 +737,7 @@ async function runGeminiDelegate(binary: string, task: string, resume: boolean,
|
|
|
357
737
|
}
|
|
358
738
|
}
|
|
359
739
|
|
|
360
|
-
async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
|
|
740
|
+
async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
|
|
361
741
|
try {
|
|
362
742
|
const env: NodeJS.ProcessEnv = stripEnvPrefixes({ ...process.env }, ['CLAUDE'])
|
|
363
743
|
const authProbe = spawnSync(binary, ['auth', 'status'], { cwd: bctx.cwd, env, encoding: 'utf-8', timeout: 8000 })
|
|
@@ -370,6 +750,7 @@ async function runClaudeDelegate(binary: string, task: string, resume: boolean,
|
|
|
370
750
|
const args = ['--print', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']
|
|
371
751
|
if (resumeIdToUse) args.push('--resume', resumeIdToUse)
|
|
372
752
|
const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
753
|
+
bindDelegateRuntime(runtime, child)
|
|
373
754
|
let stderr = ''
|
|
374
755
|
let assistantText = ''
|
|
375
756
|
let discoveredId: string | null = null
|
|
@@ -426,18 +807,23 @@ const DelegatePlugin: Plugin = {
|
|
|
426
807
|
tools: [
|
|
427
808
|
{
|
|
428
809
|
name: 'delegate',
|
|
429
|
-
description: 'Delegate to a specialized backend (Claude, Codex, OpenCode, Gemini).',
|
|
810
|
+
description: 'Delegate to a specialized backend (Claude, Codex, OpenCode, Gemini). Supports background jobs with action=status|list|wait|cancel.',
|
|
430
811
|
parameters: {
|
|
431
812
|
type: 'object',
|
|
432
813
|
properties: {
|
|
814
|
+
action: { type: 'string', enum: ['start', 'status', 'list', 'wait', 'cancel'] },
|
|
433
815
|
task: { type: 'string' },
|
|
434
816
|
backend: { type: 'string', enum: ['claude', 'codex', 'opencode', 'gemini'] },
|
|
435
817
|
resume: { type: 'boolean' },
|
|
436
|
-
resumeId: { type: 'string', description: 'Optional explicit session/thread ID to resume' }
|
|
818
|
+
resumeId: { type: 'string', description: 'Optional explicit session/thread ID to resume' },
|
|
819
|
+
jobId: { type: 'string' },
|
|
820
|
+
waitForCompletion: { type: 'boolean' },
|
|
821
|
+
background: { type: 'boolean' },
|
|
822
|
+
timeoutSec: { type: 'number' },
|
|
437
823
|
},
|
|
438
|
-
required: [
|
|
824
|
+
required: []
|
|
439
825
|
},
|
|
440
|
-
execute: async (args, context) => executeDelegateAction(args,
|
|
826
|
+
execute: async (args, context) => executeDelegateAction(args, buildDelegateContextFromSessionish(context.session))
|
|
441
827
|
}
|
|
442
828
|
]
|
|
443
829
|
}
|
|
@@ -96,8 +96,8 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
96
96
|
})
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
-
const {
|
|
100
|
-
|
|
99
|
+
const { requestApprovalMaybeAutoApprove } = await import('../approvals')
|
|
100
|
+
const approval = await requestApprovalMaybeAutoApprove({
|
|
101
101
|
category: 'tool_access',
|
|
102
102
|
title: `Enable Plugin: ${pluginId}`,
|
|
103
103
|
description: reason || `Agent is requesting access to the "${pluginId}" plugin.`,
|
|
@@ -105,6 +105,15 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
105
105
|
agentId: bctx?.ctx?.agentId,
|
|
106
106
|
sessionId: bctx?.ctx?.sessionId,
|
|
107
107
|
})
|
|
108
|
+
if (approval.status === 'approved') {
|
|
109
|
+
return JSON.stringify({
|
|
110
|
+
alreadyGranted: true,
|
|
111
|
+
pluginId,
|
|
112
|
+
toolId: pluginId,
|
|
113
|
+
autoApproved: true,
|
|
114
|
+
message: `Access to "${pluginId}" was auto-approved and granted. Proceed to use it directly.`,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
108
117
|
return JSON.stringify({
|
|
109
118
|
type: 'plugin_request',
|
|
110
119
|
pluginId,
|
|
@@ -118,8 +127,8 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
118
127
|
return JSON.stringify({ error: 'url is required for install_request.' })
|
|
119
128
|
}
|
|
120
129
|
if (approved !== true) {
|
|
121
|
-
const {
|
|
122
|
-
|
|
130
|
+
const { requestApprovalMaybeAutoApprove } = await import('../approvals')
|
|
131
|
+
const approval = await requestApprovalMaybeAutoApprove({
|
|
123
132
|
category: 'plugin_install',
|
|
124
133
|
title: `Install Plugin${pluginId ? `: ${pluginId}` : ' from URL'}`,
|
|
125
134
|
description: reason || `Agent wants to install a plugin${url ? ` from ${url}` : ''}.`,
|
|
@@ -127,6 +136,15 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
127
136
|
agentId: bctx?.ctx?.agentId,
|
|
128
137
|
sessionId: bctx?.ctx?.sessionId,
|
|
129
138
|
})
|
|
139
|
+
if (approval.status === 'approved') {
|
|
140
|
+
return JSON.stringify({
|
|
141
|
+
type: 'plugin_install_request',
|
|
142
|
+
url,
|
|
143
|
+
pluginId,
|
|
144
|
+
autoApproved: true,
|
|
145
|
+
message: `Plugin install from ${url} was auto-approved and has been applied.`,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
130
148
|
return JSON.stringify({
|
|
131
149
|
type: 'plugin_install_request',
|
|
132
150
|
url,
|