@swarmclawai/swarmclaw 0.6.7 → 0.7.0

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.
Files changed (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -32,11 +32,13 @@ export function streamClaudeCliChat({ session, message, imagePath, systemPrompt,
32
32
  }
33
33
 
34
34
  const args = ['--print', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']
35
- if (session.claudeSessionId) args.push('--resume', session.claudeSessionId)
36
- if (session.model) args.push('--model', session.model)
35
+ const resumeSessionId = typeof session.claudeSessionId === 'string' ? session.claudeSessionId : ''
36
+ const selectedModel = typeof session.model === 'string' ? session.model : ''
37
+ if (resumeSessionId) args.push('--resume', resumeSessionId)
38
+ if (selectedModel) args.push('--model', selectedModel)
37
39
 
38
40
  // Inject agent system prompt
39
- if (systemPrompt && !session.claudeSessionId) {
41
+ if (systemPrompt && !resumeSessionId) {
40
42
  args.push('--system-prompt', systemPrompt)
41
43
  }
42
44
 
@@ -5,7 +5,7 @@ import { streamOpenAiChat } from './openai'
5
5
  import { streamOllamaChat } from './ollama'
6
6
  import { streamAnthropicChat } from './anthropic'
7
7
  import { streamOpenClawChat } from './openclaw'
8
- import type { ProviderInfo, ProviderConfig as CustomProviderConfig } from '../../types'
8
+ import type { ProviderInfo, ProviderConfig as CustomProviderConfig, ProviderType } from '../../types'
9
9
 
10
10
  const RETRYABLE_STATUS_CODES = [401, 429, 500, 502, 503]
11
11
 
@@ -19,14 +19,16 @@ export interface StreamChatUsage {
19
19
  }
20
20
 
21
21
  export interface StreamChatOptions {
22
- session: any
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ session: Record<string, any> & { id: string }
23
24
  message: string
24
25
  imagePath?: string
25
26
  imageUrl?: string
26
27
  apiKey?: string | null
27
28
  systemPrompt?: string
28
29
  write: (data: string) => void
29
- active: Map<string, any>
30
+ active: Map<string, unknown>
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
32
  loadHistory: (sessionId: string) => any[]
31
33
  onUsage?: (usage: StreamChatUsage) => void
32
34
  /** Abort signal from the caller — providers should use this to cancel in-flight requests. */
@@ -37,7 +39,7 @@ interface BuiltinProviderConfig extends ProviderInfo {
37
39
  handler: ProviderHandler
38
40
  }
39
41
 
40
- const PROVIDERS: Record<string, BuiltinProviderConfig> = {
42
+ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
41
43
  'claude-cli': {
42
44
  id: 'claude-cli',
43
45
  name: 'Claude Code CLI',
@@ -232,6 +234,7 @@ const PROVIDERS: Record<string, BuiltinProviderConfig> = {
232
234
  /** Merge built-in providers with custom providers from storage */
233
235
  function getCustomProviders(): Record<string, CustomProviderConfig> {
234
236
  try {
237
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
235
238
  const { loadProviderConfigs } = require('../server/storage')
236
239
  return loadProviderConfigs() as Record<string, CustomProviderConfig>
237
240
  } catch {
@@ -241,6 +244,7 @@ function getCustomProviders(): Record<string, CustomProviderConfig> {
241
244
 
242
245
  function getModelOverrides(): Record<string, string[]> {
243
246
  try {
247
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
244
248
  const { loadModelOverrides } = require('../server/storage')
245
249
  return loadModelOverrides()
246
250
  } catch {
@@ -257,41 +261,80 @@ export function getProviderList(): ProviderInfo[] {
257
261
  models: overrides[info.id] || info.models,
258
262
  defaultModels: info.models,
259
263
  }))
260
- const customs = Object.values(getCustomProviders())
264
+
265
+ const customs: ProviderInfo[] = Object.values(getCustomProviders())
261
266
  .filter((c) => c.isEnabled)
262
267
  .map((c) => ({
263
- id: c.id as any,
268
+ id: c.id as ProviderType,
264
269
  name: c.name,
265
270
  models: c.models,
266
271
  defaultModels: c.models,
267
272
  requiresApiKey: c.requiresApiKey,
268
- requiresEndpoint: false,
273
+ requiresEndpoint: false as boolean,
269
274
  defaultEndpoint: c.baseUrl,
270
275
  }))
271
- return [...builtins, ...customs]
276
+
277
+ let pluginProviders: ProviderInfo[] = []
278
+ try {
279
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
280
+ const { getPluginManager } = require('../server/plugins')
281
+ pluginProviders = getPluginManager().getProviders().map((p: Record<string, unknown>) => ({
282
+ id: String(p.id) as ProviderType,
283
+ name: String(p.name),
284
+ models: p.models as string[],
285
+ defaultModels: p.models as string[],
286
+ requiresApiKey: Boolean(p.requiresApiKey),
287
+ requiresEndpoint: Boolean(p.requiresEndpoint),
288
+ defaultEndpoint: p.defaultEndpoint as string | undefined,
289
+ }))
290
+ } catch { /* ignore if running somewhere plugins aren't available */ }
291
+
292
+ return [...builtins, ...customs, ...pluginProviders]
272
293
  }
273
294
 
274
295
  export function getProvider(id: string): BuiltinProviderConfig | null {
275
296
  if (PROVIDERS[id]) return PROVIDERS[id]
276
- // Check custom providers — they use OpenAI-compatible handler with custom baseUrl
297
+
298
+ // Check custom providers
277
299
  const customs = getCustomProviders()
278
300
  const custom = customs[id]
279
301
  if (custom?.isEnabled) {
280
302
  return {
281
- id: custom.id as any,
303
+ id: custom.id as ProviderType,
282
304
  name: custom.name,
283
305
  models: custom.models,
284
306
  requiresApiKey: custom.requiresApiKey,
285
307
  requiresEndpoint: false,
286
308
  handler: {
287
- streamChat: (opts) => {
288
- // Custom providers use OpenAI handler with custom baseUrl
309
+ streamChat: async (opts) => {
289
310
  const patchedSession = { ...opts.session, apiEndpoint: custom.baseUrl }
311
+ const { streamOpenAiChat } = await import('./openai')
290
312
  return streamOpenAiChat({ ...opts, session: patchedSession })
291
313
  },
292
314
  },
293
315
  }
294
316
  }
317
+
318
+ // Check Plugin Providers
319
+ try {
320
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
321
+ const { getPluginManager } = require('../server/plugins')
322
+ const pluginProviders = getPluginManager().getProviders()
323
+ const found = pluginProviders.find((p: Record<string, unknown>) => p.id === id)
324
+ if (found) {
325
+ return {
326
+ id: found.id as ProviderType,
327
+ name: found.name,
328
+ models: found.models,
329
+ requiresApiKey: found.requiresApiKey,
330
+ requiresEndpoint: found.requiresEndpoint,
331
+ handler: {
332
+ streamChat: found.streamChat
333
+ }
334
+ }
335
+ }
336
+ } catch { /* ignore */ }
337
+
295
338
  return null
296
339
  }
297
340
 
@@ -315,7 +358,7 @@ export async function streamChatWithFailover(
315
358
  return provider.handler.streamChat(opts)
316
359
  }
317
360
 
318
- let lastError: any = null
361
+ let lastError: unknown = null
319
362
 
320
363
  for (let i = 0; i < credentialIds.length; i++) {
321
364
  const credId = credentialIds[i]
@@ -324,6 +367,7 @@ export async function streamChatWithFailover(
324
367
  let apiKey: string | null = opts.apiKey || null
325
368
  if (credId && i > 0) {
326
369
  // Need to decrypt fallback credential
370
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
327
371
  const { loadCredentials, decryptKey } = require('../server/storage')
328
372
  const creds = loadCredentials()
329
373
  const cred = creds[credId]
@@ -337,21 +381,23 @@ export async function streamChatWithFailover(
337
381
  apiKey,
338
382
  })
339
383
  return result // success
340
- } catch (err: any) {
384
+ } catch (err: unknown) {
341
385
  lastError = err
342
- const statusCode = err.status || err.statusCode || 0
386
+ const errObj = err as Record<string, unknown>
387
+ const statusCode = (typeof errObj?.status === 'number' ? errObj.status : typeof errObj?.statusCode === 'number' ? errObj.statusCode : 0) as number
388
+ const errMessage = err instanceof Error ? err.message : String(err)
343
389
  const isRetryable = RETRYABLE_STATUS_CODES.includes(statusCode)
344
- || err.message?.includes('rate limit')
345
- || err.message?.includes('Rate limit')
346
- || err.message?.includes('429')
347
- || err.message?.includes('401')
390
+ || errMessage?.includes('rate limit')
391
+ || errMessage?.includes('Rate limit')
392
+ || errMessage?.includes('429')
393
+ || errMessage?.includes('401')
348
394
 
349
395
  if (isRetryable && i < credentialIds.length - 1) {
350
- console.log(`[failover] Credential ${credId} failed (${statusCode || err.message}), trying fallback...`)
396
+ console.log(`[failover] Credential ${credId} failed (${statusCode || errMessage}), trying fallback...`)
351
397
  // Send a metadata event to inform the client
352
398
  opts.write(`data: ${JSON.stringify({
353
399
  t: 'md',
354
- text: JSON.stringify({ failover: { from: credId, reason: err.message?.slice(0, 100) } }),
400
+ text: JSON.stringify({ failover: { from: credId, reason: errMessage?.slice(0, 100) } }),
355
401
  })}\n\n`)
356
402
  // Exponential backoff for rate-limit / server errors (skip for auth rotation)
357
403
  if (statusCode !== 401) {
@@ -3,11 +3,12 @@ import type { LoopMode } from '@/types'
3
3
  export const DEFAULT_LOOP_MODE: LoopMode = 'bounded'
4
4
 
5
5
  // Loop limits
6
- export const DEFAULT_AGENT_LOOP_RECURSION_LIMIT = 15
7
- export const DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT = 25
6
+ export const DEFAULT_AGENT_LOOP_RECURSION_LIMIT = 30
7
+ export const DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT = 40
8
8
  export const DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS = 10
9
9
  export const DEFAULT_ONGOING_LOOP_MAX_ITERATIONS = 250
10
10
  export const DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES = 60
11
+ export const DEFAULT_DELEGATION_MAX_DEPTH = 3
11
12
 
12
13
  // Tool/process timeouts
13
14
  export const DEFAULT_SHELL_COMMAND_TIMEOUT_SEC = 30
@@ -0,0 +1,150 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { genId } from '@/lib/id'
4
+ import { loadApprovals, upsertApproval, loadSessions, saveSessions } from './storage'
5
+ import type { ApprovalRequest, ApprovalCategory } from '@/types'
6
+ import { notify } from './ws-hub'
7
+ import { DATA_DIR } from './data-dir'
8
+ import { log } from './logger'
9
+
10
+ function getApprovalTargetId(data: Record<string, unknown>): string | null {
11
+ const toolId = typeof data.toolId === 'string' ? data.toolId.trim() : ''
12
+ if (toolId) return toolId
13
+ const pluginId = typeof data.pluginId === 'string' ? data.pluginId.trim() : ''
14
+ return pluginId || null
15
+ }
16
+
17
+ export function requestApproval(params: {
18
+ category: ApprovalCategory
19
+ title: string
20
+ description?: string
21
+ data: Record<string, unknown>
22
+ agentId?: string | null
23
+ sessionId?: string | null
24
+ taskId?: string | null
25
+ }): ApprovalRequest {
26
+ const targetId = getApprovalTargetId(params.data)
27
+ if (params.category === 'tool_access' && !targetId) {
28
+ throw new Error('tool_access approvals require a toolId or pluginId')
29
+ }
30
+
31
+ const normalizedData = { ...params.data }
32
+ if (params.category === 'tool_access' && targetId) {
33
+ normalizedData.toolId = targetId
34
+ normalizedData.pluginId = targetId
35
+ }
36
+
37
+ const id = genId(8)
38
+ const now = Date.now()
39
+ const request: ApprovalRequest = {
40
+ id,
41
+ ...params,
42
+ title: params.category === 'tool_access' && targetId ? `Enable Plugin: ${targetId}` : params.title,
43
+ data: normalizedData,
44
+ status: 'pending',
45
+ createdAt: now,
46
+ updatedAt: now,
47
+ }
48
+
49
+ upsertApproval(id, request)
50
+
51
+ notify('approvals')
52
+ return request
53
+ }
54
+
55
+
56
+ export async function submitDecision(id: string, approved: boolean): Promise<void> {
57
+ const approvals = loadApprovals() as Record<string, ApprovalRequest>
58
+ const request = approvals[id]
59
+ if (!request) throw new Error('Approval request not found')
60
+
61
+ request.status = approved ? 'approved' : 'rejected'
62
+ request.updatedAt = Date.now()
63
+ upsertApproval(id, request)
64
+
65
+ // Handle specific side effects based on category
66
+ if (approved) {
67
+ if (request.category === 'tool_access' && request.sessionId) {
68
+ const sessions = loadSessions()
69
+ const session = sessions[request.sessionId]
70
+ if (session) {
71
+ const toolId = getApprovalTargetId(request.data)
72
+ const currentTools = session.tools || []
73
+ if (toolId && !currentTools.includes(toolId)) {
74
+ session.tools = [...currentTools, toolId]
75
+ saveSessions(sessions)
76
+ }
77
+ }
78
+ }
79
+
80
+ if (request.category === 'plugin_scaffold') {
81
+ const filename = typeof request.data.filename === 'string' ? request.data.filename : ''
82
+ const code = typeof request.data.code === 'string' ? request.data.code : ''
83
+ if (filename && code) {
84
+ const pluginsDir = path.join(DATA_DIR, 'plugins')
85
+ if (!fs.existsSync(pluginsDir)) fs.mkdirSync(pluginsDir, { recursive: true })
86
+ fs.writeFileSync(path.join(pluginsDir, filename), code, 'utf8')
87
+ const { getPluginManager } = await import('./plugins')
88
+ getPluginManager().reload()
89
+
90
+ // Store creator agent metadata
91
+ const createdByAgentId = typeof request.data.createdByAgentId === 'string' ? request.data.createdByAgentId : request.agentId
92
+ if (createdByAgentId) {
93
+ getPluginManager().setMeta(filename, { createdByAgentId })
94
+ }
95
+ log.info('approvals', `Plugin scaffolded: ${filename}`)
96
+
97
+ // Auto-enable the new plugin for the creating agent's session
98
+ if (request.sessionId) {
99
+ const sessions = loadSessions()
100
+ const session = sessions[request.sessionId]
101
+ if (session) {
102
+ const currentTools = session.tools || []
103
+ if (!currentTools.includes(filename)) {
104
+ session.tools = [...currentTools, filename]
105
+ saveSessions(sessions)
106
+ }
107
+ }
108
+ }
109
+ notify('plugins')
110
+ }
111
+ }
112
+
113
+ if (request.category === 'plugin_install') {
114
+ const url = typeof request.data.url === 'string' ? request.data.url : ''
115
+ if (url) {
116
+ try {
117
+ const res = await fetch(url, { signal: AbortSignal.timeout(15000) })
118
+ if (res.ok) {
119
+ const code = await res.text()
120
+ const pluginId = typeof request.data.pluginId === 'string' ? request.data.pluginId : ''
121
+ const safeName = (pluginId || url.split('/').pop() || 'plugin').replace(/[^a-zA-Z0-9._-]/g, '_')
122
+ const filename = safeName.endsWith('.js') ? safeName : `${safeName}.js`
123
+ const pluginsDir = path.join(DATA_DIR, 'plugins')
124
+ if (!fs.existsSync(pluginsDir)) fs.mkdirSync(pluginsDir, { recursive: true })
125
+ fs.writeFileSync(path.join(pluginsDir, filename), code, 'utf8')
126
+ const { getPluginManager } = await import('./plugins')
127
+ getPluginManager().reload()
128
+ log.info('approvals', `Plugin installed from URL: ${filename}`)
129
+ notify('plugins')
130
+ }
131
+ } catch (err: unknown) {
132
+ log.error('approvals', 'Plugin install failed after approval', {
133
+ url,
134
+ error: err instanceof Error ? err.message : String(err),
135
+ })
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ notify('approvals')
142
+ if (request.sessionId) notify(`session:${request.sessionId}`)
143
+ }
144
+
145
+ export function listPendingApprovals(): ApprovalRequest[] {
146
+ const approvals = loadApprovals() as Record<string, ApprovalRequest>
147
+ return Object.values(approvals)
148
+ .filter(a => a.status === 'pending')
149
+ .sort((a, b) => b.updatedAt - a.updatedAt)
150
+ }