@swarmclawai/swarmclaw 0.7.3 → 0.7.4

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 (147) hide show
  1. package/README.md +47 -40
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +17 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +3 -1
  12. package/src/app/api/agents/route.ts +23 -1
  13. package/src/app/api/auth/route.ts +1 -1
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/route.ts +12 -0
  19. package/src/app/api/chats/heartbeat/route.ts +2 -1
  20. package/src/app/api/chats/route.ts +7 -1
  21. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  22. package/src/app/api/external-agents/[id]/route.ts +31 -0
  23. package/src/app/api/external-agents/register/route.ts +3 -0
  24. package/src/app/api/external-agents/route.ts +66 -0
  25. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  26. package/src/app/api/gateways/[id]/route.ts +79 -0
  27. package/src/app/api/gateways/route.ts +57 -0
  28. package/src/app/api/openclaw/gateway/route.ts +10 -7
  29. package/src/app/api/openclaw/skills/route.ts +1 -1
  30. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  31. package/src/app/api/schedules/[id]/route.ts +38 -9
  32. package/src/app/api/schedules/route.ts +51 -28
  33. package/src/app/api/settings/route.ts +6 -10
  34. package/src/app/api/setup/doctor/route.ts +6 -4
  35. package/src/app/api/tasks/[id]/route.ts +2 -1
  36. package/src/app/api/tasks/bulk/route.ts +2 -2
  37. package/src/app/page.tsx +126 -15
  38. package/src/cli/binary.test.js +142 -0
  39. package/src/cli/index.js +34 -11
  40. package/src/cli/index.test.js +195 -0
  41. package/src/cli/index.ts +20 -4
  42. package/src/cli/server-cmd.test.js +59 -0
  43. package/src/cli/spec.js +20 -2
  44. package/src/components/agents/agent-sheet.tsx +249 -7
  45. package/src/components/agents/inspector-panel.tsx +3 -2
  46. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  47. package/src/components/auth/setup-wizard.tsx +970 -275
  48. package/src/components/chat/chat-area.tsx +41 -14
  49. package/src/components/chat/chat-card.tsx +2 -1
  50. package/src/components/chat/chat-header.tsx +8 -13
  51. package/src/components/chat/chat-list.tsx +58 -20
  52. package/src/components/chat/message-list.tsx +142 -18
  53. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  54. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  55. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  56. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +157 -86
  59. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  60. package/src/components/gateways/gateway-sheet.tsx +567 -0
  61. package/src/components/input/chat-input.tsx +135 -86
  62. package/src/components/layout/app-layout.tsx +2 -0
  63. package/src/components/memory/memory-browser.tsx +71 -6
  64. package/src/components/memory/memory-card.tsx +18 -0
  65. package/src/components/memory/memory-detail.tsx +58 -31
  66. package/src/components/memory/memory-sheet.tsx +32 -4
  67. package/src/components/projects/project-detail.tsx +7 -2
  68. package/src/components/providers/provider-list.tsx +158 -2
  69. package/src/components/providers/provider-sheet.tsx +81 -70
  70. package/src/components/shared/bottom-sheet.tsx +31 -15
  71. package/src/components/shared/confirm-dialog.tsx +45 -30
  72. package/src/components/shared/model-combobox.tsx +90 -8
  73. package/src/components/shared/settings/section-heartbeat.tsx +11 -6
  74. package/src/components/shared/settings/section-orchestrator.tsx +3 -0
  75. package/src/components/shared/settings/settings-page.tsx +5 -3
  76. package/src/components/tasks/approvals-panel.tsx +7 -1
  77. package/src/components/ui/dialog.tsx +2 -2
  78. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  79. package/src/lib/heartbeat-defaults.ts +48 -0
  80. package/src/lib/memory-presentation.ts +59 -0
  81. package/src/lib/provider-model-discovery-client.ts +29 -0
  82. package/src/lib/providers/index.ts +12 -5
  83. package/src/lib/runtime-loop.ts +105 -3
  84. package/src/lib/safe-storage.ts +6 -1
  85. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  86. package/src/lib/server/agent-runtime-config.ts +277 -0
  87. package/src/lib/server/approvals-auto-approve.test.ts +59 -0
  88. package/src/lib/server/build-llm.test.ts +13 -5
  89. package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
  90. package/src/lib/server/chat-execution.ts +159 -71
  91. package/src/lib/server/chatroom-helpers.test.ts +7 -0
  92. package/src/lib/server/chatroom-helpers.ts +99 -6
  93. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  94. package/src/lib/server/connectors/manager.ts +89 -61
  95. package/src/lib/server/connectors/slack.ts +1 -1
  96. package/src/lib/server/daemon-state.ts +3 -2
  97. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  98. package/src/lib/server/eval/agent-regression.ts +1742 -0
  99. package/src/lib/server/eval/runner.ts +11 -1
  100. package/src/lib/server/eval/store.ts +2 -1
  101. package/src/lib/server/heartbeat-service.ts +10 -4
  102. package/src/lib/server/main-agent-loop.ts +13 -6
  103. package/src/lib/server/openclaw-exec-config.ts +4 -2
  104. package/src/lib/server/openclaw-gateway.ts +123 -36
  105. package/src/lib/server/orchestrator-lg.ts +1 -2
  106. package/src/lib/server/orchestrator.ts +3 -2
  107. package/src/lib/server/plugins.test.ts +9 -1
  108. package/src/lib/server/plugins.ts +12 -2
  109. package/src/lib/server/provider-model-discovery.ts +481 -0
  110. package/src/lib/server/queue.ts +1 -1
  111. package/src/lib/server/runtime-settings.test.ts +119 -0
  112. package/src/lib/server/runtime-settings.ts +12 -92
  113. package/src/lib/server/schedule-normalization.ts +187 -0
  114. package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
  115. package/src/lib/server/session-tools/crud.ts +27 -3
  116. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  117. package/src/lib/server/session-tools/discovery.ts +18 -8
  118. package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
  119. package/src/lib/server/session-tools/file.ts +8 -2
  120. package/src/lib/server/session-tools/http.ts +9 -3
  121. package/src/lib/server/session-tools/index.ts +31 -1
  122. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  123. package/src/lib/server/session-tools/monitor.ts +14 -7
  124. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  125. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  126. package/src/lib/server/session-tools/platform.ts +1 -1
  127. package/src/lib/server/session-tools/plugin-creator.ts +9 -2
  128. package/src/lib/server/session-tools/sandbox.ts +51 -92
  129. package/src/lib/server/session-tools/session-info.ts +22 -1
  130. package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
  131. package/src/lib/server/session-tools/shell.ts +2 -2
  132. package/src/lib/server/session-tools/subagent.ts +3 -1
  133. package/src/lib/server/session-tools/web.ts +73 -30
  134. package/src/lib/server/storage.ts +29 -3
  135. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  136. package/src/lib/server/stream-agent-chat.ts +139 -4
  137. package/src/lib/server/structured-extract.ts +1 -1
  138. package/src/lib/server/task-mention.ts +0 -1
  139. package/src/lib/server/tool-aliases.ts +37 -6
  140. package/src/lib/server/tool-capability-policy.ts +1 -1
  141. package/src/lib/setup-defaults.ts +352 -11
  142. package/src/lib/tool-definitions.ts +3 -4
  143. package/src/lib/validation/schemas.ts +55 -1
  144. package/src/stores/use-app-store.ts +43 -1
  145. package/src/stores/use-chatroom-store.ts +153 -26
  146. package/src/types/index.ts +189 -6
  147. package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
@@ -14,22 +14,20 @@ function getDenoPath(): string | null {
14
14
  return findBinaryOnPath('deno')
15
15
  }
16
16
 
17
- function getNodePath(): string | null {
18
- return findBinaryOnPath('node')
19
- }
20
-
21
- function getTsxPath(): string | null {
22
- return findBinaryOnPath('tsx')
23
- }
24
-
25
- function getPythonPath(): string | null {
26
- return findBinaryOnPath('python3') ?? findBinaryOnPath('python')
27
- }
28
-
29
17
  const EXT_MAP: Record<string, string> = {
30
18
  javascript: 'js',
31
19
  typescript: 'ts',
32
- python: 'py',
20
+ }
21
+
22
+ function sandboxUnavailableError(reason: string): string {
23
+ return JSON.stringify({
24
+ error: reason,
25
+ guidance: [
26
+ 'Install Deno or run `npm run setup:easy` to enable sandbox_exec.',
27
+ 'Use http_request for straightforward API calls.',
28
+ 'Use plugin_creator plus manage_schedules for recurring automations.',
29
+ ],
30
+ })
33
31
  }
34
32
 
35
33
  /**
@@ -45,18 +43,13 @@ async function executeSandboxExec(args: any, context: { sessionId?: string; cwd?
45
43
  const sessionId = context.sessionId ?? 'unknown'
46
44
  const sandboxDir = path.join('/tmp', `swarmclaw-sandbox-${sessionId}-${Date.now()}`)
47
45
  const denoPath = getDenoPath()
48
- const nodePath = getNodePath()
49
- const tsxPath = getTsxPath()
50
- const pythonPath = getPythonPath()
51
46
 
52
- if (language === 'javascript' && !denoPath && !nodePath) {
53
- return JSON.stringify({ error: 'No JavaScript runtime available. Install Deno or Node.js.' })
54
- }
55
- if (language === 'typescript' && !denoPath && !tsxPath) {
56
- return JSON.stringify({ error: 'No TypeScript runtime available. Install Deno or tsx.' })
47
+ if (language !== 'javascript' && language !== 'typescript') {
48
+ return sandboxUnavailableError('sandbox_exec currently supports only JavaScript and TypeScript via Deno.')
57
49
  }
58
- if (language === 'python' && !pythonPath) {
59
- return JSON.stringify({ error: 'Python is not installed.' })
50
+
51
+ if (!denoPath) {
52
+ return sandboxUnavailableError('Deno is required for sandbox_exec. Unsafe Node/Python fallbacks are disabled.')
60
53
  }
61
54
 
62
55
  try {
@@ -65,36 +58,15 @@ async function executeSandboxExec(args: any, context: { sessionId?: string; cwd?
65
58
  const scriptPath = path.join(sandboxDir, scriptFile)
66
59
  fs.writeFileSync(scriptPath, code, 'utf-8')
67
60
 
68
- let result: ReturnType<typeof spawnSync>
69
-
70
- if (language === 'javascript') {
71
- if (denoPath) {
72
- result = spawnSync(denoPath, [
73
- 'run', '--allow-read=.', '--allow-write=.', '--allow-net', '--deny-env', '--no-prompt', scriptFile,
74
- ], { cwd: sandboxDir, encoding: 'utf-8', timeout, maxBuffer: MAX_OUTPUT })
75
- } else {
76
- result = spawnSync(nodePath!, [scriptPath], {
77
- cwd: sandboxDir, encoding: 'utf-8', timeout, maxBuffer: MAX_OUTPUT,
78
- env: { PATH: process.env.PATH || '/usr/bin:/bin' } as any,
79
- })
80
- }
81
- } else if (language === 'typescript') {
82
- if (denoPath) {
83
- result = spawnSync(denoPath, [
84
- 'run', '--allow-read=.', '--allow-write=.', '--allow-net', '--deny-env', '--no-prompt', scriptFile,
85
- ], { cwd: sandboxDir, encoding: 'utf-8', timeout, maxBuffer: MAX_OUTPUT })
86
- } else {
87
- result = spawnSync(tsxPath!, [scriptPath], {
88
- cwd: sandboxDir, encoding: 'utf-8', timeout, maxBuffer: MAX_OUTPUT,
89
- env: { PATH: process.env.PATH || '/usr/bin:/bin' } as any,
90
- })
91
- }
92
- } else {
93
- result = spawnSync(pythonPath!, [scriptPath], {
94
- cwd: sandboxDir, encoding: 'utf-8', timeout, maxBuffer: MAX_OUTPUT,
95
- env: { PATH: process.env.PATH || '/usr/bin:/bin' } as any,
96
- })
97
- }
61
+ const result = spawnSync(denoPath, [
62
+ 'run',
63
+ '--allow-read=.',
64
+ '--allow-write=.',
65
+ '--allow-net',
66
+ '--deny-env',
67
+ '--no-prompt',
68
+ scriptFile,
69
+ ], { cwd: sandboxDir, encoding: 'utf-8', timeout, maxBuffer: MAX_OUTPUT })
98
70
 
99
71
  const stdout = truncate((result.stdout || '').toString(), MAX_OUTPUT)
100
72
  const stderr = truncate((result.stderr || '').toString(), MAX_OUTPUT)
@@ -125,16 +97,18 @@ async function executeSandboxExec(args: any, context: { sessionId?: string; cwd?
125
97
  }
126
98
 
127
99
  async function executeListRuntimes() {
128
- const runtimes: Record<string, any> = {}
129
- for (const [name, bin] of [['deno', getDenoPath()], ['node', getNodePath()], ['tsx', getTsxPath()], ['python', getPythonPath()]] as const) {
130
- if (bin) {
131
- const ver = spawnSync(bin, ['--version'], { encoding: 'utf-8', timeout: 3000 })
132
- runtimes[name] = { available: true, version: (ver.stdout || '').split('\n')[0]?.trim() || null }
133
- } else {
134
- runtimes[name] = { available: false }
135
- }
100
+ const denoPath = getDenoPath()
101
+ if (!denoPath) {
102
+ return sandboxUnavailableError('Deno is not available for sandbox_exec.')
136
103
  }
137
- return JSON.stringify(runtimes)
104
+ const ver = spawnSync(denoPath, ['--version'], { encoding: 'utf-8', timeout: 3000 })
105
+ return JSON.stringify({
106
+ deno: {
107
+ available: true,
108
+ version: (ver.stdout || '').split('\n')[0]?.trim() || null,
109
+ },
110
+ sandboxReady: true,
111
+ })
138
112
  }
139
113
 
140
114
  /**
@@ -142,18 +116,23 @@ async function executeListRuntimes() {
142
116
  */
143
117
  const SandboxPlugin: Plugin = {
144
118
  name: 'Core Sandbox',
145
- description: 'Secure isolated code execution for JS, TS, and Python.',
119
+ description: 'Deno-based isolated code execution for JavaScript and TypeScript when custom code is necessary.',
146
120
  hooks: {
147
- getCapabilityDescription: () => 'I can run code in a sandbox (`sandbox_exec`) JS/TS via Deno or Python, in an isolated environment. I get stdout, stderr, and any files created.',
121
+ getCapabilityDescription: () => 'I can run JavaScript or TypeScript in a Deno sandbox (`sandbox_exec`) when custom code is necessary. For straightforward API calls, use `http_request` instead.',
122
+ getOperatingGuidance: () => [
123
+ 'Use `http_request` for straightforward REST/JSON API calls instead of writing code in `sandbox_exec`.',
124
+ 'Use `sandbox_exec` only when custom parsing or transformation code is actually needed.',
125
+ 'For recurring automations, prefer `plugin_creator` plus `manage_schedules` over repeated sandbox runs.',
126
+ ],
148
127
  } as PluginHooks,
149
128
  tools: [
150
129
  {
151
130
  name: 'sandbox_exec',
152
- description: 'Execute code in an isolated sandbox.',
131
+ description: 'Execute JavaScript or TypeScript in a Deno sandbox when custom code is necessary.',
153
132
  parameters: {
154
133
  type: 'object',
155
134
  properties: {
156
- language: { type: 'string', enum: ['javascript', 'typescript', 'python'] },
135
+ language: { type: 'string', enum: ['javascript', 'typescript'] },
157
136
  code: { type: 'string' },
158
137
  timeoutSec: { type: 'number' }
159
138
  },
@@ -163,7 +142,7 @@ const SandboxPlugin: Plugin = {
163
142
  },
164
143
  {
165
144
  name: 'sandbox_list_runtimes',
166
- description: 'List available sandbox runtimes.',
145
+ description: 'Report whether the Deno sandbox runtime is available.',
167
146
  parameters: { type: 'object', properties: {} },
168
147
  execute: async () => executeListRuntimes()
169
148
  }
@@ -185,7 +164,11 @@ export function buildSandboxTools(bctx: ToolBuildContext): StructuredToolInterfa
185
164
  {
186
165
  name: 'sandbox_exec',
187
166
  description: SandboxPlugin.tools![0].description,
188
- schema: z.object({}).passthrough()
167
+ schema: z.object({
168
+ language: z.enum(['javascript', 'typescript']),
169
+ code: z.string(),
170
+ timeoutSec: z.number().optional(),
171
+ })
189
172
  }
190
173
  ),
191
174
  tool(
@@ -198,29 +181,5 @@ export function buildSandboxTools(bctx: ToolBuildContext): StructuredToolInterfa
198
181
  )
199
182
  )
200
183
 
201
- const openclawPath = findBinaryOnPath('openclaw') || findBinaryOnPath('clawdbot')
202
- if (openclawPath) {
203
- tools.push(
204
- tool(
205
- async (rawArgs) => {
206
- const normalized = normalizeToolInputArgs((rawArgs ?? {}) as Record<string, unknown>)
207
- const code = normalized.code as string | undefined
208
- const explain = normalized.explain as boolean | undefined
209
- try {
210
- if (!code) return JSON.stringify({ error: 'code is required' })
211
- const args = explain ? ['sandbox', 'explain', code] : ['sandbox', 'run', code]
212
- const result = spawnSync(openclawPath, args, { encoding: 'utf-8', timeout: 60_000, maxBuffer: MAX_OUTPUT })
213
- return JSON.stringify({ exitCode: result.status ?? 0, stdout: truncate(result.stdout || '', MAX_OUTPUT), stderr: truncate(result.stderr || '', MAX_OUTPUT) })
214
- } catch (err: any) { return JSON.stringify({ error: err.message }) }
215
- },
216
- {
217
- name: 'openclaw_sandbox',
218
- description: 'Execute or explain code through OpenClaw CLI.',
219
- schema: z.object({ code: z.string(), explain: z.boolean().optional() }),
220
- }
221
- )
222
- )
223
- }
224
-
225
184
  return tools
226
185
  }
@@ -25,9 +25,30 @@ async function executeWhoAmI(context: { sessionId?: string; agentId?: string })
25
25
  } catch (err: any) { return `Error: ${err.message}` }
26
26
  }
27
27
 
28
+ function inferSessionsAction(
29
+ normalized: Record<string, unknown>,
30
+ context: { sessionId?: string; agentId?: string },
31
+ ): string | undefined {
32
+ const explicit = typeof normalized.action === 'string' ? normalized.action.trim() : ''
33
+ if (explicit) return explicit
34
+
35
+ const hasUpdates = !!normalized.updates && typeof normalized.updates === 'object'
36
+ const hasSpawnTarget = typeof normalized.agentId === 'string' || typeof normalized.agent_id === 'string'
37
+ const hasHistoryTarget =
38
+ typeof normalized.sessionId === 'string'
39
+ || typeof normalized.session_id === 'string'
40
+ || typeof normalized.limit === 'number'
41
+ || !!context.sessionId
42
+
43
+ if (hasUpdates) return 'update'
44
+ if (hasSpawnTarget) return 'spawn'
45
+ if (hasHistoryTarget) return 'history'
46
+ return 'list'
47
+ }
48
+
28
49
  async function executeSessionsAction(args: any, context: { sessionId?: string; agentId?: string; cwd: string }) {
29
50
  const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
30
- const action = normalized.action as string | undefined
51
+ const action = inferSessionsAction(normalized, context)
31
52
  const sessionId = (normalized.sessionId ?? normalized.session_id) as string | undefined
32
53
  const message = normalized.message as string | undefined
33
54
  const limit = normalized.limit as number | undefined
@@ -32,12 +32,14 @@ describe('module exports', () => {
32
32
  const crawl = await import('./crawl')
33
33
  const mailbox = await import('./mailbox')
34
34
  const humanLoop = await import('./human-loop')
35
+ const sandbox = await import('./sandbox')
35
36
  assert.equal(typeof document.buildDocumentTools, 'function')
36
37
  assert.equal(typeof extract.buildExtractTools, 'function')
37
38
  assert.equal(typeof table.buildTableTools, 'function')
38
39
  assert.equal(typeof crawl.buildCrawlTools, 'function')
39
40
  assert.equal(typeof mailbox.buildMailboxTools, 'function')
40
41
  assert.equal(typeof humanLoop.buildHumanLoopTools, 'function')
42
+ assert.equal(typeof sandbox.buildSandboxTools, 'function')
41
43
  })
42
44
  })
43
45
 
@@ -72,6 +74,27 @@ describe('buildSessionTools signature', () => {
72
74
  // Verify the function has arity of at least 2
73
75
  assert.ok(buildSessionTools.length >= 2, 'buildSessionTools should accept at least 2 params')
74
76
  })
77
+
78
+ it('sandbox builder exposes only the local Deno sandbox tools', async () => {
79
+ const { buildSandboxTools } = await import('./sandbox')
80
+ const bctx: import('./context').ToolBuildContext = {
81
+ cwd: process.cwd(),
82
+ ctx: { sessionId: 'sandbox-test' },
83
+ hasPlugin: (name) => name === 'sandbox',
84
+ hasTool: (name) => name === 'sandbox',
85
+ cleanupFns: [],
86
+ commandTimeoutMs: 1_000,
87
+ claudeTimeoutMs: 1_000,
88
+ cliProcessTimeoutMs: 1_000,
89
+ persistDelegateResumeId: () => {},
90
+ readStoredDelegateResumeId: () => null,
91
+ resolveCurrentSession: () => null,
92
+ activePlugins: ['sandbox'],
93
+ }
94
+
95
+ const tools = buildSandboxTools(bctx).map((tool) => tool.name).sort()
96
+ assert.deepEqual(tools, ['sandbox_exec', 'sandbox_list_runtimes'])
97
+ })
75
98
  })
76
99
 
77
100
  // ---------------------------------------------------------------------------
@@ -163,8 +163,8 @@ const ShellPlugin: Plugin = {
163
163
  name: 'Core Shell',
164
164
  description: 'Execute shell commands and manage background processes.',
165
165
  hooks: {
166
- getCapabilityDescription: () => 'I can run shell commands (`execute_command`) servers, installs, scripts, git, builds, anything. I can run things in the background for long-lived processes like dev servers.',
167
- getOperatingGuidance: () => ['Shell: use `execute_command` for servers, installs, scripts, git. Use `background=true` for long-lived processes.', 'Verify servers with `process_tool` status/log and liveness probes before claiming success.', 'Resolve IPs/URLs via shell — never use placeholders. Retry path errors without workdir override.'],
166
+ getCapabilityDescription: () => 'I can run shell commands with the unified `shell` tool. Use action `execute` for commands, and `list` / `status` / `poll` / `log` for long-lived processes.',
167
+ getOperatingGuidance: () => ['Shell: use `shell` with `{"action":"execute","command":"..."}` for servers, installs, scripts, and git. Use `background=true` for long-lived processes.', 'Verify servers with `shell` status/log actions and liveness probes before claiming success.', 'Resolve IPs/URLs via shell — never use placeholders. Retry path errors without workdir override.'],
168
168
  } as PluginHooks,
169
169
  tools: [
170
170
  {
@@ -9,6 +9,7 @@ import type { ToolBuildContext } from './context'
9
9
  import type { Plugin, PluginHooks } from '@/types'
10
10
  import { getPluginManager } from '../plugins'
11
11
  import { normalizeToolInputArgs } from './normalize-tool-args'
12
+ import { applyResolvedRoute, resolvePrimaryAgentRoute } from '../agent-runtime-config'
12
13
  import {
13
14
  appendDelegationCheckpoint,
14
15
  cancelDelegationJob,
@@ -74,7 +75,7 @@ async function startSubagentJob(jobId: string, args: {
74
75
  const sessions = loadSessions()
75
76
  const parent = context.sessionId ? sessions[context.sessionId] : null
76
77
  const browserProfileId = resolveSubagentBrowserProfileId(parent, sid, args.shareBrowserProfile === true)
77
- sessions[sid] = {
78
+ const nextSession = {
78
79
  id: sid,
79
80
  name: `subagent-${agent.name}`,
80
81
  cwd: args.cwd || context.cwd,
@@ -91,6 +92,7 @@ async function startSubagentJob(jobId: string, args: {
91
92
  plugins: agent.plugins || agent.tools || [],
92
93
  browserProfileId,
93
94
  }
95
+ sessions[sid] = applyResolvedRoute(nextSession, resolvePrimaryAgentRoute(agent))
94
96
  saveSessions(sessions)
95
97
 
96
98
  startDelegationJob(jobId, {
@@ -222,7 +222,7 @@ const WebPlugin: Plugin = {
222
222
  name: 'Core Web',
223
223
  description: 'Search the web and fetch content from URLs.',
224
224
  hooks: {
225
- getCapabilityDescription: () => 'I can search the web (`web_search`) for research, fact-checking, and discovery.',
225
+ getCapabilityDescription: () => 'I can use the unified `web` tool with action `search` for research and action `fetch` for reading a URL.',
226
226
  } as PluginHooks,
227
227
  tools: [
228
228
  {
@@ -400,9 +400,12 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
400
400
  }
401
401
 
402
402
  const stringifyStructured = (value: unknown): string => truncate(JSON.stringify(value, null, 2), MAX_OUTPUT)
403
+ const callBrowserEvaluate = (fn: string) => callMcpTool('browser_evaluate', {
404
+ function: fn,
405
+ })
403
406
 
404
407
  const captureStructuredObservation = async () => {
405
- const expression = `(() => {
408
+ const expression = `() => {
406
409
  const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
407
410
  const visible = (el) => {
408
411
  if (!el) return false;
@@ -445,17 +448,27 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
445
448
  .slice(0, 10)
446
449
  .map((el) => normalize(el.innerText || el.textContent))
447
450
  .filter(Boolean);
448
- return JSON.stringify({
451
+ const textPreview = normalize(document.body?.innerText || document.body?.textContent || '').slice(0, 1200);
452
+ const lowerPreview = textPreview.toLowerCase();
453
+ const notices = [];
454
+ if (/ask the human|out-of-band|do not guess|verification code required/.test(lowerPreview)) {
455
+ notices.push({
456
+ type: 'human_input_required',
457
+ message: 'This page requires human-provided input. Ask the human instead of guessing or repeatedly submitting blank values.',
458
+ });
459
+ }
460
+ return {
449
461
  url: window.location.href,
450
462
  title: document.title || null,
451
- textPreview: normalize(document.body?.innerText || document.body?.textContent || '').slice(0, 1200),
463
+ textPreview,
452
464
  links,
453
465
  forms,
454
466
  tables,
455
467
  errors,
456
- });
457
- })()`
458
- const raw = await callMcpTool('browser_evaluate', { expression })
468
+ notices,
469
+ };
470
+ }`
471
+ const raw = await callBrowserEvaluate(expression)
459
472
  const parsed = extractJsonPayload(raw)
460
473
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
461
474
  const observation = {
@@ -638,18 +651,39 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
638
651
 
639
652
  const dismissCookieBanners = async (mcpCall: (toolName: string, args: Record<string, unknown>) => Promise<string>) => {
640
653
  await new Promise((r) => setTimeout(r, 1500))
641
- const js = `(() => {
654
+ const js = `() => {
642
655
  const sel = ['button[id*="reject" i]', 'button[class*="reject" i]', 'a[id*="reject" i]', 'a[class*="reject" i]', '#onetrust-reject-all-handler', '#CybotCookiebotDialogBodyButtonDecline', '#didomi-notice-disagree-button', '.qc-cmp2-summary-buttons button:first-child', 'button.sp_choice_type_12'];
643
656
  for (const s of sel) { const el = document.querySelector(s); if (el && el.offsetParent !== null) { el.click(); return 'dismissed:' + s; } }
644
657
  const btns = [...document.querySelectorAll('button, a[role="button"]')]; const rejectRe = /^(reject|reject all|decline|deny|refuse|no,? thanks|only necessary|necessary only)$/i;
645
658
  for (const b of btns) { const txt = (b.textContent || '').trim(); if (rejectRe.test(txt) && b.offsetParent !== null) { b.click(); return 'dismissed:text=' + txt; } }
646
659
  return 'none';
647
- })()`
648
- await mcpCall('browser_evaluate', { expression: js })
660
+ }`
661
+ await mcpCall('browser_evaluate', { function: js })
649
662
  }
650
663
 
651
664
  const performFillForm = async (params: Record<string, unknown>) => {
652
- const fields = Array.isArray(params.fields) ? params.fields : []
665
+ const fields = Array.isArray(params.fields)
666
+ ? params.fields
667
+ : (() => {
668
+ const form = params.form
669
+ if (!form || typeof form !== 'object' || Array.isArray(form)) return []
670
+ return Object.entries(form as Record<string, unknown>).map(([key, value]) => {
671
+ const escapedId = String(key).replace(/[^a-zA-Z0-9_-]/g, '')
672
+ const escapedAttr = String(key).replace(/["\\]/g, '\\$&')
673
+ const inferredType = typeof value === 'boolean'
674
+ ? 'checkbox'
675
+ : /password/i.test(key)
676
+ ? 'password'
677
+ : 'text'
678
+ return {
679
+ element: escapedId
680
+ ? `#${escapedId}, [name="${escapedAttr}"]`
681
+ : `[name="${escapedAttr}"]`,
682
+ type: inferredType,
683
+ value,
684
+ }
685
+ })
686
+ })()
653
687
  if (fields.length === 0) return { ok: false, error: 'fields is required for fill_form.' }
654
688
  const filled: Array<Record<string, unknown>> = []
655
689
  for (const field of fields) {
@@ -692,14 +726,29 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
692
726
  element: typeof params.submitElement === 'string' ? params.submitElement : undefined,
693
727
  })
694
728
  } else {
695
- await callMcpTool('browser_press_key', { key: typeof params.key === 'string' ? params.key : 'Enter' })
729
+ await callBrowserEvaluate(`() => {
730
+ const form = document.forms[0];
731
+ if (!form) return { submitted: false, reason: 'no-form' };
732
+ const submitButton = form.querySelector('button[type="submit"], input[type="submit"], button');
733
+ if (submitButton && typeof submitButton.click === 'function') {
734
+ submitButton.click();
735
+ return { submitted: true, method: 'click' };
736
+ }
737
+ if (typeof form.requestSubmit === 'function') {
738
+ form.requestSubmit();
739
+ return { submitted: true, method: 'requestSubmit' };
740
+ }
741
+ if (typeof form.submit === 'function') {
742
+ form.submit();
743
+ return { submitted: true, method: 'submit' };
744
+ }
745
+ return { submitted: false, reason: 'no-submit-method' };
746
+ }`)
696
747
  }
697
748
 
698
749
  const waitMs = typeof params.waitMs === 'number' ? Math.max(250, params.waitMs) : 1000
699
750
  try {
700
- await callMcpTool('browser_evaluate', {
701
- expression: `await new Promise(resolve => setTimeout(resolve, ${Math.min(waitMs, 5000)}))`,
702
- })
751
+ await callBrowserEvaluate(`async () => { await new Promise(resolve => setTimeout(resolve, ${Math.min(waitMs, 5000)})); }`)
703
752
  } catch {
704
753
  await new Promise((resolve) => setTimeout(resolve, waitMs))
705
754
  }
@@ -723,18 +772,16 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
723
772
  const maxScrolls = typeof params.maxScrolls === 'number' ? Math.max(1, Math.min(20, params.maxScrolls)) : 8
724
773
  let matchedAtStep = -1
725
774
  for (let index = 0; index < maxScrolls; index += 1) {
726
- const result = await callMcpTool('browser_evaluate', {
727
- expression: `(() => {
775
+ const result = await callBrowserEvaluate(`() => {
728
776
  const bodyText = String(document.body?.innerText || document.body?.textContent || '');
729
777
  const selector = ${JSON.stringify(selector)};
730
778
  const containsText = ${JSON.stringify(containsText)};
731
779
  const match = (selector && !!document.querySelector(selector))
732
780
  || (containsText && bodyText.includes(containsText));
733
- if (match) return JSON.stringify({ found: true, scrollY: window.scrollY, step: ${index} });
781
+ if (match) return { found: true, scrollY: window.scrollY, step: ${index} };
734
782
  window.scrollBy({ top: Math.max(window.innerHeight * 0.85, 600), behavior: 'instant' });
735
- return JSON.stringify({ found: false, scrollY: window.scrollY, step: ${index} });
736
- })()`,
737
- })
783
+ return { found: false, scrollY: window.scrollY, step: ${index} };
784
+ }`)
738
785
  const payload = extractJsonPayload(result)
739
786
  if (payload && typeof payload === 'object' && !Array.isArray(payload) && (payload as Record<string, unknown>).found === true) {
740
787
  matchedAtStep = index
@@ -756,8 +803,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
756
803
  const linkText = typeof params.linkText === 'string' ? params.linkText.trim() : ''
757
804
  const hrefContains = typeof params.hrefContains === 'string' ? params.hrefContains.trim() : ''
758
805
  if (!linkText && !hrefContains) return null
759
- const result = await callMcpTool('browser_evaluate', {
760
- expression: `(() => {
806
+ const result = await callBrowserEvaluate(`() => {
761
807
  const linkText = ${JSON.stringify(linkText)};
762
808
  const hrefContains = ${JSON.stringify(hrefContains)};
763
809
  const links = Array.from(document.querySelectorAll('a[href]'));
@@ -769,9 +815,8 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
769
815
  if (hrefContains && href.toLowerCase().includes(hrefContains.toLowerCase())) return true;
770
816
  return false;
771
817
  });
772
- return JSON.stringify({ href: match ? (match.href || match.getAttribute('href') || '') : null });
773
- })()`,
774
- })
818
+ return { href: match ? (match.href || match.getAttribute('href') || '') : null };
819
+ }`)
775
820
  const payload = extractJsonPayload(result)
776
821
  if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
777
822
  const href = (payload as Record<string, unknown>).href
@@ -1083,16 +1128,14 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
1083
1128
 
1084
1129
  if (action === 'screenshot' || action === 'snapshot') {
1085
1130
  try {
1086
- await callMcpTool('browser_evaluate', {
1087
- expression: `await new Promise(resolve => {
1131
+ await callBrowserEvaluate(`async () => { await new Promise(resolve => {
1088
1132
  if (document.readyState === 'complete') {
1089
1133
  setTimeout(resolve, 1200);
1090
1134
  } else {
1091
1135
  window.addEventListener('load', () => setTimeout(resolve, 1200), { once: true });
1092
1136
  setTimeout(resolve, 5000);
1093
1137
  }
1094
- })`,
1095
- })
1138
+ }); }`)
1096
1139
  } catch {
1097
1140
  await new Promise((r) => setTimeout(r, 1200))
1098
1141
  }
@@ -5,7 +5,9 @@ import os from 'os'
5
5
  import Database from 'better-sqlite3'
6
6
 
7
7
  import { DATA_DIR, WORKSPACE_DIR } from './data-dir'
8
- import type { Message } from '@/types'
8
+ import { normalizeHeartbeatSettingFields } from '@/lib/heartbeat-defaults'
9
+ import { normalizeRuntimeSettingFields } from '@/lib/runtime-loop'
10
+ import type { ExternalAgentRuntime, GatewayProfile, Message } from '@/types'
9
11
  export const UPLOAD_DIR = path.join(DATA_DIR, 'uploads')
10
12
 
11
13
  // --- LRU Cache ---
@@ -135,6 +137,7 @@ const COLLECTIONS = [
135
137
  'tasks',
136
138
  'secrets',
137
139
  'provider_configs',
140
+ 'gateway_profiles',
138
141
  'skills',
139
142
  'connectors',
140
143
  'documents',
@@ -159,6 +162,7 @@ const COLLECTIONS = [
159
162
  'browser_sessions',
160
163
  'watch_jobs',
161
164
  'delegation_jobs',
165
+ 'external_agents',
162
166
  ] as const
163
167
 
164
168
  export type StorageCollection = (typeof COLLECTIONS)[number]
@@ -352,10 +356,12 @@ const JSON_FILES: Record<string, string> = {
352
356
  tasks: path.join(DATA_DIR, 'tasks.json'),
353
357
  secrets: path.join(DATA_DIR, 'secrets.json'),
354
358
  provider_configs: path.join(DATA_DIR, 'providers.json'),
359
+ gateway_profiles: path.join(DATA_DIR, 'gateways.json'),
355
360
  skills: path.join(DATA_DIR, 'skills.json'),
356
361
  connectors: path.join(DATA_DIR, 'connectors.json'),
357
362
  documents: path.join(DATA_DIR, 'documents.json'),
358
363
  webhooks: path.join(DATA_DIR, 'webhooks.json'),
364
+ external_agents: path.join(DATA_DIR, 'external-agents.json'),
359
365
  }
360
366
 
361
367
  const MIGRATION_FLAG = path.join(DATA_DIR, '.sqlite_migrated')
@@ -803,6 +809,8 @@ function isProvidedSecretValue(value: unknown): value is string {
803
809
 
804
810
  function buildPersistedSettings(input: Record<string, any>, existing?: PersistedSettingsRecord): PersistedSettingsRecord {
805
811
  const next = cloneRecord(input) as PersistedSettingsRecord
812
+ Object.assign(next, normalizeRuntimeSettingFields(next))
813
+ Object.assign(next, normalizeHeartbeatSettingFields(next))
806
814
  const encrypted = {
807
815
  ...(existing ? getEncryptedAppSettings(existing) : {}),
808
816
  ...getEncryptedAppSettings(next),
@@ -935,6 +943,15 @@ export function saveProviderConfigs(p: Record<string, any>) {
935
943
  saveCollection('provider_configs', p)
936
944
  }
937
945
 
946
+ // --- Gateway Profiles ---
947
+ export function loadGatewayProfiles(): Record<string, any> {
948
+ return loadCollection('gateway_profiles') as Record<string, GatewayProfile>
949
+ }
950
+
951
+ export function saveGatewayProfiles(g: Record<string, GatewayProfile>) {
952
+ saveCollection('gateway_profiles', g)
953
+ }
954
+
938
955
  // --- Model Overrides (user-added models for built-in providers) ---
939
956
  export function loadModelOverrides(): Record<string, string[]> {
940
957
  return loadCollection('model_overrides') as Record<string, string[]>
@@ -964,6 +981,15 @@ export function saveSkills(s: Record<string, any>) {
964
981
  saveCollection('skills', s)
965
982
  }
966
983
 
984
+ // --- External Agent Runtimes ---
985
+ export function loadExternalAgents(): Record<string, ExternalAgentRuntime> {
986
+ return loadCollection('external_agents') as Record<string, ExternalAgentRuntime>
987
+ }
988
+
989
+ export function saveExternalAgents(items: Record<string, ExternalAgentRuntime>) {
990
+ saveCollection('external_agents', items)
991
+ }
992
+
967
993
  // --- Usage ---
968
994
  export function loadUsage(): Record<string, any[]> {
969
995
  const stmt = db.prepare('SELECT session_id, data FROM usage')
@@ -1067,11 +1093,11 @@ export function saveIntegrityBaselines(entries: Record<string, any>) {
1067
1093
  }
1068
1094
 
1069
1095
  // --- Webhook Logs ---
1070
- export function loadWebhookLogs(): Record<string, any> {
1096
+ export function loadWebhookLogs(): Record<string, unknown> {
1071
1097
  return loadCollection('webhook_logs')
1072
1098
  }
1073
1099
 
1074
- export function appendWebhookLog(id: string, entry: any) {
1100
+ export function appendWebhookLog(id: string, entry: unknown) {
1075
1101
  upsertCollectionItem('webhook_logs', id, entry)
1076
1102
  }
1077
1103