@swarmclawai/swarmclaw 0.7.3 → 0.7.5
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 +47 -40
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +17 -0
- package/src/app/api/agents/[id]/thread/route.ts +4 -87
- package/src/app/api/agents/route.ts +23 -1
- package/src/app/api/auth/route.ts +1 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/route.ts +12 -0
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +1 -1
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +6 -10
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +2 -1
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/page.tsx +126 -15
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +34 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +20 -4
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-sheet.tsx +249 -7
- package/src/components/agents/inspector-panel.tsx +3 -2
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +41 -14
- package/src/components/chat/chat-card.tsx +2 -1
- package/src/components/chat/chat-header.tsx +8 -13
- package/src/components/chat/chat-list.tsx +58 -20
- package/src/components/chat/message-list.tsx +142 -18
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +157 -86
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +2 -0
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/projects/project-detail.tsx +7 -2
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/section-heartbeat.tsx +11 -6
- package/src/components/shared/settings/section-orchestrator.tsx +3 -0
- package/src/components/shared/settings/settings-page.tsx +5 -3
- package/src/components/tasks/approvals-panel.tsx +7 -1
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/agent-thread-session.test.ts +85 -0
- package/src/lib/server/agent-thread-session.ts +123 -0
- package/src/lib/server/approvals-auto-approve.test.ts +59 -0
- package/src/lib/server/build-llm.test.ts +13 -5
- package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
- package/src/lib/server/chat-execution.ts +159 -71
- package/src/lib/server/chatroom-helpers.test.ts +7 -0
- package/src/lib/server/chatroom-helpers.ts +99 -6
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/manager.ts +89 -61
- package/src/lib/server/connectors/slack.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -2
- package/src/lib/server/data-dir.test.ts +56 -0
- package/src/lib/server/data-dir.ts +15 -9
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -8
- package/src/lib/server/heartbeat-wake.ts +6 -2
- package/src/lib/server/main-agent-loop.ts +13 -6
- package/src/lib/server/openclaw-exec-config.ts +4 -2
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/orchestrator-lg.ts +1 -2
- package/src/lib/server/orchestrator.ts +3 -2
- package/src/lib/server/plugins.test.ts +9 -1
- package/src/lib/server/plugins.ts +12 -2
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +1 -1
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
- package/src/lib/server/session-tools/crud.ts +27 -3
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +18 -8
- package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
- package/src/lib/server/session-tools/file.ts +8 -2
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/index.ts +31 -1
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/monitor.ts +14 -7
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +9 -2
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/session-info.ts +22 -1
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +3 -1
- package/src/lib/server/session-tools/web.ts +73 -30
- package/src/lib/server/storage.ts +29 -3
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +139 -4
- package/src/lib/server/structured-extract.ts +1 -1
- package/src/lib/server/task-mention.ts +0 -1
- package/src/lib/server/tool-aliases.ts +37 -6
- package/src/lib/server/tool-capability-policy.ts +1 -1
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.ts +55 -1
- package/src/stores/use-app-store.ts +43 -1
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +189 -6
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import type { LoopMode } from '@/types'
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
DEFAULT_CLAUDE_CODE_TIMEOUT_SEC,
|
|
5
|
-
DEFAULT_CLI_PROCESS_TIMEOUT_SEC,
|
|
6
|
-
DEFAULT_DELEGATION_MAX_DEPTH,
|
|
7
|
-
DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS,
|
|
8
|
-
DEFAULT_LOOP_MODE,
|
|
9
|
-
DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
|
|
10
|
-
DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES,
|
|
11
|
-
DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT,
|
|
12
|
-
DEFAULT_SHELL_COMMAND_TIMEOUT_SEC,
|
|
3
|
+
normalizeRuntimeSettingFields,
|
|
13
4
|
} from '@/lib/runtime-loop'
|
|
14
5
|
import { loadSettings } from './storage'
|
|
15
6
|
|
|
@@ -26,92 +17,21 @@ export interface RuntimeSettings {
|
|
|
26
17
|
cliProcessTimeoutMs: number
|
|
27
18
|
}
|
|
28
19
|
|
|
29
|
-
function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
|
|
30
|
-
const parsed = typeof value === 'number'
|
|
31
|
-
? value
|
|
32
|
-
: typeof value === 'string'
|
|
33
|
-
? Number.parseInt(value, 10)
|
|
34
|
-
: Number.NaN
|
|
35
|
-
if (!Number.isFinite(parsed)) return fallback
|
|
36
|
-
const int = Math.trunc(parsed)
|
|
37
|
-
return Math.max(min, Math.min(max, int))
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseLoopMode(value: unknown): LoopMode {
|
|
41
|
-
return value === 'ongoing' ? 'ongoing' : DEFAULT_LOOP_MODE
|
|
42
|
-
}
|
|
43
|
-
|
|
44
20
|
export function loadRuntimeSettings(): RuntimeSettings {
|
|
45
21
|
const settings = loadSettings()
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const agentLoopRecursionLimit = parseIntSetting(
|
|
49
|
-
settings.agentLoopRecursionLimit,
|
|
50
|
-
DEFAULT_AGENT_LOOP_RECURSION_LIMIT,
|
|
51
|
-
1,
|
|
52
|
-
200,
|
|
53
|
-
)
|
|
54
|
-
const orchestratorLoopRecursionLimit = parseIntSetting(
|
|
55
|
-
settings.orchestratorLoopRecursionLimit,
|
|
56
|
-
DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT,
|
|
57
|
-
1,
|
|
58
|
-
300,
|
|
59
|
-
)
|
|
60
|
-
const legacyOrchestratorMaxTurns = parseIntSetting(
|
|
61
|
-
settings.legacyOrchestratorMaxTurns,
|
|
62
|
-
DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS,
|
|
63
|
-
1,
|
|
64
|
-
300,
|
|
65
|
-
)
|
|
66
|
-
const delegationMaxDepth = parseIntSetting(
|
|
67
|
-
settings.delegationMaxDepth,
|
|
68
|
-
DEFAULT_DELEGATION_MAX_DEPTH,
|
|
69
|
-
1,
|
|
70
|
-
12,
|
|
71
|
-
)
|
|
72
|
-
const ongoingLoopMaxIterations = parseIntSetting(
|
|
73
|
-
settings.ongoingLoopMaxIterations,
|
|
74
|
-
DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
|
|
75
|
-
10,
|
|
76
|
-
5000,
|
|
77
|
-
)
|
|
78
|
-
const ongoingLoopMaxRuntimeMinutes = parseIntSetting(
|
|
79
|
-
settings.ongoingLoopMaxRuntimeMinutes,
|
|
80
|
-
DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES,
|
|
81
|
-
0,
|
|
82
|
-
1440,
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
const shellCommandTimeoutSec = parseIntSetting(
|
|
86
|
-
settings.shellCommandTimeoutSec,
|
|
87
|
-
DEFAULT_SHELL_COMMAND_TIMEOUT_SEC,
|
|
88
|
-
1,
|
|
89
|
-
600,
|
|
90
|
-
)
|
|
91
|
-
const claudeCodeTimeoutSec = parseIntSetting(
|
|
92
|
-
settings.claudeCodeTimeoutSec,
|
|
93
|
-
DEFAULT_CLAUDE_CODE_TIMEOUT_SEC,
|
|
94
|
-
5,
|
|
95
|
-
7200,
|
|
96
|
-
)
|
|
97
|
-
const cliProcessTimeoutSec = parseIntSetting(
|
|
98
|
-
settings.cliProcessTimeoutSec,
|
|
99
|
-
DEFAULT_CLI_PROCESS_TIMEOUT_SEC,
|
|
100
|
-
10,
|
|
101
|
-
7200,
|
|
102
|
-
)
|
|
22
|
+
const normalized = normalizeRuntimeSettingFields(settings)
|
|
103
23
|
|
|
104
24
|
return {
|
|
105
|
-
loopMode,
|
|
106
|
-
agentLoopRecursionLimit,
|
|
107
|
-
orchestratorLoopRecursionLimit,
|
|
108
|
-
legacyOrchestratorMaxTurns,
|
|
109
|
-
delegationMaxDepth,
|
|
110
|
-
ongoingLoopMaxIterations,
|
|
111
|
-
ongoingLoopMaxRuntimeMs: ongoingLoopMaxRuntimeMinutes > 0 ? ongoingLoopMaxRuntimeMinutes * 60_000 : null,
|
|
112
|
-
shellCommandTimeoutMs: shellCommandTimeoutSec * 1000,
|
|
113
|
-
claudeCodeTimeoutMs: claudeCodeTimeoutSec * 1000,
|
|
114
|
-
cliProcessTimeoutMs: cliProcessTimeoutSec * 1000,
|
|
25
|
+
loopMode: normalized.loopMode as LoopMode,
|
|
26
|
+
agentLoopRecursionLimit: normalized.agentLoopRecursionLimit,
|
|
27
|
+
orchestratorLoopRecursionLimit: normalized.orchestratorLoopRecursionLimit,
|
|
28
|
+
legacyOrchestratorMaxTurns: normalized.legacyOrchestratorMaxTurns,
|
|
29
|
+
delegationMaxDepth: normalized.delegationMaxDepth,
|
|
30
|
+
ongoingLoopMaxIterations: normalized.ongoingLoopMaxIterations,
|
|
31
|
+
ongoingLoopMaxRuntimeMs: normalized.ongoingLoopMaxRuntimeMinutes > 0 ? normalized.ongoingLoopMaxRuntimeMinutes * 60_000 : null,
|
|
32
|
+
shellCommandTimeoutMs: normalized.shellCommandTimeoutSec * 1000,
|
|
33
|
+
claudeCodeTimeoutMs: normalized.claudeCodeTimeoutSec * 1000,
|
|
34
|
+
cliProcessTimeoutMs: normalized.cliProcessTimeoutSec * 1000,
|
|
115
35
|
}
|
|
116
36
|
}
|
|
117
37
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { WORKSPACE_DIR } from './data-dir'
|
|
4
|
+
|
|
5
|
+
type SchedulePayload = Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
export interface NormalizeScheduleOptions {
|
|
8
|
+
cwd?: string | null
|
|
9
|
+
now?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type NormalizeScheduleResult =
|
|
13
|
+
| { ok: true; value: SchedulePayload }
|
|
14
|
+
| { ok: false; error: string }
|
|
15
|
+
|
|
16
|
+
const SCRIPT_FILE_EXT = /\.(py|js|mjs|cjs|ts|tsx|sh|bash|zsh|rb|php|pl)$/i
|
|
17
|
+
const DIRECT_SCRIPT_RUNNERS = new Set(['python', 'python3', 'python3.11', 'node', 'bash', 'sh', 'zsh', 'ruby', 'tsx', 'ts-node'])
|
|
18
|
+
const VALID_STATUSES = new Set(['active', 'paused', 'completed', 'failed'])
|
|
19
|
+
|
|
20
|
+
function trimString(value: unknown): string {
|
|
21
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeScheduleType(value: unknown): 'cron' | 'interval' | 'once' {
|
|
25
|
+
if (value === 'cron' || value === 'interval' || value === 'once') return value
|
|
26
|
+
return 'interval'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizePositiveInt(value: unknown): number | null {
|
|
30
|
+
const parsed = typeof value === 'number'
|
|
31
|
+
? value
|
|
32
|
+
: typeof value === 'string'
|
|
33
|
+
? Number.parseInt(value, 10)
|
|
34
|
+
: Number.NaN
|
|
35
|
+
if (!Number.isFinite(parsed)) return null
|
|
36
|
+
const intValue = Math.trunc(parsed)
|
|
37
|
+
return intValue > 0 ? intValue : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isWithinDirectory(parent: string, child: string): boolean {
|
|
41
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child))
|
|
42
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveRelativePath(baseDir: string, candidate: string): string | null {
|
|
46
|
+
const trimmed = trimString(candidate)
|
|
47
|
+
if (!trimmed) return null
|
|
48
|
+
if (path.isAbsolute(trimmed)) {
|
|
49
|
+
const resolvedAbsolute = path.resolve(trimmed)
|
|
50
|
+
return isWithinDirectory(baseDir, resolvedAbsolute) ? resolvedAbsolute : null
|
|
51
|
+
}
|
|
52
|
+
const resolved = path.resolve(baseDir, trimmed)
|
|
53
|
+
return isWithinDirectory(baseDir, resolved) ? resolved : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tokenizeCommand(command: string): string[] {
|
|
57
|
+
return String(command || '').match(/(?:[^\s"'`]+|"[^"]*"|'[^']*')+/g) || []
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function unquoteToken(token: string): string {
|
|
61
|
+
if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith('\'') && token.endsWith('\''))) {
|
|
62
|
+
return token.slice(1, -1)
|
|
63
|
+
}
|
|
64
|
+
return token
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function looksLikeScriptPath(token: string): boolean {
|
|
68
|
+
return SCRIPT_FILE_EXT.test(token) || token.includes('/') || token.includes(path.sep)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractScriptPathFromCommand(command: string): string | null {
|
|
72
|
+
const tokens = tokenizeCommand(command).map(unquoteToken).filter(Boolean)
|
|
73
|
+
if (!tokens.length) return null
|
|
74
|
+
|
|
75
|
+
const commandName = path.basename(tokens[0] || '').toLowerCase()
|
|
76
|
+
let startIndex = 1
|
|
77
|
+
if (commandName === 'npx' && tokens[1]) {
|
|
78
|
+
const nestedRunner = path.basename(tokens[1]).toLowerCase()
|
|
79
|
+
if (nestedRunner === 'tsx' || nestedRunner === 'ts-node') startIndex = 2
|
|
80
|
+
} else if (commandName === 'deno' && tokens[1] === 'run') {
|
|
81
|
+
startIndex = 2
|
|
82
|
+
} else if (!DIRECT_SCRIPT_RUNNERS.has(commandName)) {
|
|
83
|
+
startIndex = 0
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (let index = startIndex; index < tokens.length; index += 1) {
|
|
87
|
+
const candidate = tokens[index]
|
|
88
|
+
if (!candidate || candidate.startsWith('-')) continue
|
|
89
|
+
if (!looksLikeScriptPath(candidate)) continue
|
|
90
|
+
return candidate
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function deriveTaskPrompt(payload: SchedulePayload): string {
|
|
97
|
+
const explicitTaskPrompt = trimString(payload.taskPrompt)
|
|
98
|
+
if (explicitTaskPrompt) return explicitTaskPrompt
|
|
99
|
+
|
|
100
|
+
const command = trimString(payload.command)
|
|
101
|
+
if (command) {
|
|
102
|
+
return `Execute the command \`${command}\` from this schedule's working directory and report the result, including any errors.`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const filePath = trimString(payload.path)
|
|
106
|
+
if (!filePath) return ''
|
|
107
|
+
|
|
108
|
+
const action = trimString(payload.action).toLowerCase()
|
|
109
|
+
if (action === 'run_script') {
|
|
110
|
+
return `Run the script at \`${filePath}\` from this schedule's working directory and report the result, including any errors.`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return `Use the file at \`${filePath}\` to complete this scheduled task and report the result.`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function validateScheduleArtifacts(payload: SchedulePayload, baseDir: string): string | null {
|
|
117
|
+
const action = trimString(payload.action).toLowerCase()
|
|
118
|
+
const filePath = trimString(payload.path)
|
|
119
|
+
const command = trimString(payload.command)
|
|
120
|
+
|
|
121
|
+
if (action === 'run_script' && !filePath) {
|
|
122
|
+
return 'run_script schedules require a path.'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (filePath) {
|
|
126
|
+
const resolved = resolveRelativePath(baseDir, filePath)
|
|
127
|
+
if (!resolved) return `schedule path must stay inside ${baseDir}: ${filePath}`
|
|
128
|
+
if (!fs.existsSync(resolved)) return `schedule path not found: ${filePath}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!command) return null
|
|
132
|
+
const commandScriptPath = extractScriptPathFromCommand(command)
|
|
133
|
+
if (!commandScriptPath) return null
|
|
134
|
+
const resolved = resolveRelativePath(baseDir, commandScriptPath)
|
|
135
|
+
if (!resolved) return `schedule command references a path outside ${baseDir}: ${commandScriptPath}`
|
|
136
|
+
if (!fs.existsSync(resolved)) return `schedule command references a missing file: ${commandScriptPath}`
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function normalizeSchedulePayload(payload: SchedulePayload, opts: NormalizeScheduleOptions = {}): NormalizeScheduleResult {
|
|
141
|
+
const now = typeof opts.now === 'number' ? opts.now : Date.now()
|
|
142
|
+
const baseDir = path.resolve(trimString(opts.cwd) || WORKSPACE_DIR)
|
|
143
|
+
const normalized: SchedulePayload = {
|
|
144
|
+
...payload,
|
|
145
|
+
scheduleType: normalizeScheduleType(payload.scheduleType),
|
|
146
|
+
}
|
|
147
|
+
const action = trimString(normalized.action)
|
|
148
|
+
const command = trimString(normalized.command)
|
|
149
|
+
const filePath = trimString(normalized.path)
|
|
150
|
+
if (action) normalized.action = action
|
|
151
|
+
if (command) normalized.command = command
|
|
152
|
+
if (filePath) normalized.path = filePath
|
|
153
|
+
|
|
154
|
+
const status = trimString(normalized.status).toLowerCase()
|
|
155
|
+
normalized.status = VALID_STATUSES.has(status) ? status : 'active'
|
|
156
|
+
|
|
157
|
+
const agentId = trimString(normalized.agentId)
|
|
158
|
+
if (!agentId) {
|
|
159
|
+
return { ok: false, error: 'Error: schedules require a target agentId.' }
|
|
160
|
+
}
|
|
161
|
+
normalized.agentId = agentId
|
|
162
|
+
|
|
163
|
+
const taskPrompt = deriveTaskPrompt(normalized)
|
|
164
|
+
if (!taskPrompt) {
|
|
165
|
+
return { ok: false, error: 'Error: schedules require a taskPrompt, command, or action/path payload.' }
|
|
166
|
+
}
|
|
167
|
+
normalized.taskPrompt = taskPrompt
|
|
168
|
+
|
|
169
|
+
const validationError = validateScheduleArtifacts(normalized, baseDir)
|
|
170
|
+
if (validationError) return { ok: false, error: `Error: ${validationError}` }
|
|
171
|
+
|
|
172
|
+
if (normalized.nextRunAt == null) {
|
|
173
|
+
if (normalized.scheduleType === 'once') {
|
|
174
|
+
const runAt = normalizePositiveInt(normalized.runAt)
|
|
175
|
+
if (runAt != null) normalized.nextRunAt = runAt
|
|
176
|
+
} else if (normalized.scheduleType === 'interval') {
|
|
177
|
+
const intervalMs = normalizePositiveInt(normalized.intervalMs)
|
|
178
|
+
if (intervalMs != null) normalized.nextRunAt = now + intervalMs
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { ok: true, value: normalized }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function extractScheduleCommandScriptPath(command: string): string | null {
|
|
186
|
+
return extractScriptPathFromCommand(command)
|
|
187
|
+
}
|
|
@@ -22,6 +22,18 @@ describe('browser workflow surface', () => {
|
|
|
22
22
|
assert.equal(src.includes(`'${action}'`), true, `web.ts should expose ${action}`)
|
|
23
23
|
}
|
|
24
24
|
})
|
|
25
|
+
|
|
26
|
+
it('supports the shorthand form-map path for fill_form', () => {
|
|
27
|
+
const src = readToolSource('web.ts')
|
|
28
|
+
assert.equal(src.includes('params.form'), true)
|
|
29
|
+
assert.equal(src.includes('fields is required for fill_form.'), true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('flags pages that require human-provided input', () => {
|
|
33
|
+
const src = readToolSource('web.ts')
|
|
34
|
+
assert.equal(src.includes("type: 'human_input_required'"), true)
|
|
35
|
+
assert.equal(src.includes('Ask the human instead of guessing'), true)
|
|
36
|
+
})
|
|
25
37
|
})
|
|
26
38
|
|
|
27
39
|
describe('durable wait surface', () => {
|
|
@@ -40,6 +52,17 @@ describe('durable wait surface', () => {
|
|
|
40
52
|
})
|
|
41
53
|
})
|
|
42
54
|
|
|
55
|
+
describe('sandbox surface', () => {
|
|
56
|
+
it('advertises a Deno-only sandbox and steers simple APIs to http_request', () => {
|
|
57
|
+
const src = readToolSource('sandbox.ts')
|
|
58
|
+
assert.equal(src.includes("enum: ['javascript', 'typescript']"), true)
|
|
59
|
+
assert.equal(src.includes('http_request'), true)
|
|
60
|
+
assert.equal(src.includes('plugin_creator'), true)
|
|
61
|
+
assert.equal(src.includes('manage_schedules'), true)
|
|
62
|
+
assert.equal(src.includes('openclaw_sandbox'), false)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
43
66
|
describe('delegation job handles', () => {
|
|
44
67
|
it('exposes subagent control actions', () => {
|
|
45
68
|
const src = readToolSource('subagent.ts')
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
validateManagedAgentAssignment,
|
|
31
31
|
} from '@/lib/server/agent-assignment'
|
|
32
32
|
import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
|
|
33
|
+
import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
|
|
33
34
|
import type { ToolBuildContext } from './context'
|
|
34
35
|
import { safePath, findBinaryOnPath } from './context'
|
|
35
36
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
@@ -261,6 +262,9 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
261
262
|
if (!hasPlugin(toolKey)) continue
|
|
262
263
|
|
|
263
264
|
let description = `Manage SwarmClaw ${res.label}. ${res.readOnly ? 'List and get only.' : 'List, get, create, update, or delete.'} Returns JSON.`
|
|
265
|
+
if (toolKey.startsWith('manage_') && toolKey !== 'manage_platform') {
|
|
266
|
+
description += `\n\nUse this direct tool name exactly as shown (\`${toolKey}\`). Do not swap it for \`manage_platform\` unless that umbrella tool is separately enabled in the current session.`
|
|
267
|
+
}
|
|
264
268
|
if (toolKey === 'manage_tasks') {
|
|
265
269
|
if (assignScope === 'self') {
|
|
266
270
|
description += `\n\nYou may create tasks for yourself ("${ctx?.agentId || 'unknown'}") or leave them unassigned to track multi-step work. You cannot assign tasks to other agents unless a user enables "Assign to Other Agents" in your agent settings. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.`
|
|
@@ -271,9 +275,9 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
271
275
|
description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field. Set "platformAssignScope":"all" to let an agent delegate work across the fleet; use "self" for solo execution.`
|
|
272
276
|
} else if (toolKey === 'manage_schedules') {
|
|
273
277
|
if (assignScope === 'self') {
|
|
274
|
-
description += `\n\
|
|
278
|
+
description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set it explicitly to yourself. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).`
|
|
275
279
|
} else {
|
|
276
|
-
description += `\n\
|
|
280
|
+
description += `\n\nOmit "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}"), or set "agentId" to another agent when needed. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Provide either taskPrompt, command, or action+path. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).` + agentSummary
|
|
277
281
|
}
|
|
278
282
|
} else if (toolKey === 'manage_webhooks') {
|
|
279
283
|
description += '\n\nUse `source`, `events`, `agentId`, and `secret` when creating webhooks. Inbound calls should POST to `/api/webhooks/{id}` with header `x-webhook-secret` when a secret is configured.'
|
|
@@ -350,7 +354,9 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
350
354
|
const resolution = resolveManagedAgentAssignment(
|
|
351
355
|
parsed as Record<string, unknown>,
|
|
352
356
|
agents,
|
|
353
|
-
toolKey === 'manage_tasks'
|
|
357
|
+
toolKey === 'manage_tasks' || toolKey === 'manage_schedules'
|
|
358
|
+
? (parsed.agentId || ctx?.agentId || null)
|
|
359
|
+
: null,
|
|
354
360
|
{ allowDescription: toolKey === 'manage_tasks' },
|
|
355
361
|
)
|
|
356
362
|
const assignmentError = validateManagedAgentAssignment({
|
|
@@ -369,6 +375,12 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
369
375
|
parsed.agentId = resolution.agentId
|
|
370
376
|
}
|
|
371
377
|
if (toolKey === 'manage_schedules') {
|
|
378
|
+
const normalizedSchedule = normalizeSchedulePayload(parsed as Record<string, unknown>, {
|
|
379
|
+
cwd,
|
|
380
|
+
now,
|
|
381
|
+
})
|
|
382
|
+
if (!normalizedSchedule.ok) return normalizedSchedule.error
|
|
383
|
+
Object.assign(parsed, normalizedSchedule.value)
|
|
372
384
|
const duplicate = findDuplicateSchedule(all as Record<string, ScheduleLike>, {
|
|
373
385
|
agentId: parsed.agentId || null,
|
|
374
386
|
taskPrompt: parsed.taskPrompt || '',
|
|
@@ -552,6 +564,18 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
|
|
|
552
564
|
}
|
|
553
565
|
}
|
|
554
566
|
all[id] = { ...all[id], ...parsed, updatedAt: Date.now() }
|
|
567
|
+
if (toolKey === 'manage_schedules') {
|
|
568
|
+
const normalizedSchedule = normalizeSchedulePayload(all[id] as Record<string, unknown>, {
|
|
569
|
+
cwd,
|
|
570
|
+
now: Date.now(),
|
|
571
|
+
})
|
|
572
|
+
if (!normalizedSchedule.ok) return normalizedSchedule.error
|
|
573
|
+
all[id] = {
|
|
574
|
+
...all[id],
|
|
575
|
+
...normalizedSchedule.value,
|
|
576
|
+
updatedAt: Date.now(),
|
|
577
|
+
}
|
|
578
|
+
}
|
|
555
579
|
if (toolKey === 'manage_secrets') {
|
|
556
580
|
if (!canAccessSecret(all[id])) return 'Error: you do not have access to this secret.'
|
|
557
581
|
const nextScope = parsed.scope === 'agent'
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it } from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir(script: string) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-discovery-approval-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
},
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
})
|
|
22
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
23
|
+
const lines = (result.stdout || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
29
|
+
return JSON.parse(jsonLine || '{}')
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('discovery approval flows', () => {
|
|
36
|
+
it('request_tool_access creates a real approval and grants the tool when auto-approved', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
39
|
+
const toolsMod = await import('./src/lib/server/session-tools/index.ts')
|
|
40
|
+
const storage = storageMod.default || storageMod
|
|
41
|
+
const toolsApi = toolsMod.default || toolsMod
|
|
42
|
+
|
|
43
|
+
storage.saveSettings({ approvalsEnabled: false })
|
|
44
|
+
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
storage.saveSessions({
|
|
47
|
+
session_tools: {
|
|
48
|
+
id: 'session_tools',
|
|
49
|
+
name: 'Tool Access Test',
|
|
50
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
51
|
+
user: 'tester',
|
|
52
|
+
provider: 'openai',
|
|
53
|
+
model: 'gpt-test',
|
|
54
|
+
claudeSessionId: null,
|
|
55
|
+
messages: [],
|
|
56
|
+
createdAt: now,
|
|
57
|
+
lastActiveAt: now,
|
|
58
|
+
sessionType: 'human',
|
|
59
|
+
agentId: 'default',
|
|
60
|
+
plugins: [],
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, [], {
|
|
65
|
+
sessionId: 'session_tools',
|
|
66
|
+
agentId: 'default',
|
|
67
|
+
platformAssignScope: 'self',
|
|
68
|
+
})
|
|
69
|
+
const tool = built.tools.find((entry) => entry.name === 'request_tool_access')
|
|
70
|
+
const raw = await tool.invoke({ toolId: 'shell', reason: 'Need terminal access.' })
|
|
71
|
+
const approvals = storage.loadApprovals()
|
|
72
|
+
const session = storage.loadSessions().session_tools
|
|
73
|
+
console.log(JSON.stringify({
|
|
74
|
+
raw,
|
|
75
|
+
approvalCount: Object.keys(approvals).length,
|
|
76
|
+
plugins: session.plugins || [],
|
|
77
|
+
}))
|
|
78
|
+
`)
|
|
79
|
+
|
|
80
|
+
assert.match(String(output.raw), /auto-approved|granted/i)
|
|
81
|
+
assert.equal(output.approvalCount, 1)
|
|
82
|
+
assert.equal(output.plugins.includes('shell'), true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('manage_capabilities request_access accepts query aliases for pluginId', () => {
|
|
86
|
+
const output = runWithTempDataDir(`
|
|
87
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
88
|
+
const toolsMod = await import('./src/lib/server/session-tools/index.ts')
|
|
89
|
+
const storage = storageMod.default || storageMod
|
|
90
|
+
const toolsApi = toolsMod.default || toolsMod
|
|
91
|
+
|
|
92
|
+
storage.saveSettings({ approvalsEnabled: false })
|
|
93
|
+
|
|
94
|
+
const now = Date.now()
|
|
95
|
+
storage.saveSessions({
|
|
96
|
+
session_caps: {
|
|
97
|
+
id: 'session_caps',
|
|
98
|
+
name: 'Capabilities Test',
|
|
99
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
100
|
+
user: 'tester',
|
|
101
|
+
provider: 'openai',
|
|
102
|
+
model: 'gpt-test',
|
|
103
|
+
claudeSessionId: null,
|
|
104
|
+
messages: [],
|
|
105
|
+
createdAt: now,
|
|
106
|
+
lastActiveAt: now,
|
|
107
|
+
sessionType: 'human',
|
|
108
|
+
agentId: 'default',
|
|
109
|
+
plugins: [],
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, [], {
|
|
114
|
+
sessionId: 'session_caps',
|
|
115
|
+
agentId: 'default',
|
|
116
|
+
platformAssignScope: 'self',
|
|
117
|
+
})
|
|
118
|
+
const tool = built.tools.find((entry) => entry.name === 'manage_capabilities')
|
|
119
|
+
const raw = await tool.invoke({ action: 'request_access', query: 'shell', reason: 'Need terminal access.' })
|
|
120
|
+
const session = storage.loadSessions().session_caps
|
|
121
|
+
console.log(JSON.stringify({
|
|
122
|
+
raw,
|
|
123
|
+
plugins: session.plugins || [],
|
|
124
|
+
}))
|
|
125
|
+
`)
|
|
126
|
+
|
|
127
|
+
assert.match(String(output.raw), /auto-approved|granted/i)
|
|
128
|
+
assert.equal(output.plugins.includes('shell'), true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('granting manage_schedules does not surface the manage_platform umbrella tool', () => {
|
|
132
|
+
const output = runWithTempDataDir(`
|
|
133
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
134
|
+
const toolsMod = await import('./src/lib/server/session-tools/index.ts')
|
|
135
|
+
const storage = storageMod.default || storageMod
|
|
136
|
+
const toolsApi = toolsMod.default || toolsMod
|
|
137
|
+
|
|
138
|
+
const now = Date.now()
|
|
139
|
+
storage.saveSessions({
|
|
140
|
+
session_sched: {
|
|
141
|
+
id: 'session_sched',
|
|
142
|
+
name: 'Schedule Tool Isolation',
|
|
143
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
144
|
+
user: 'tester',
|
|
145
|
+
provider: 'openai',
|
|
146
|
+
model: 'gpt-test',
|
|
147
|
+
claudeSessionId: null,
|
|
148
|
+
messages: [],
|
|
149
|
+
createdAt: now,
|
|
150
|
+
lastActiveAt: now,
|
|
151
|
+
sessionType: 'human',
|
|
152
|
+
agentId: 'default',
|
|
153
|
+
plugins: ['manage_schedules'],
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['manage_schedules'], {
|
|
158
|
+
sessionId: 'session_sched',
|
|
159
|
+
agentId: 'default',
|
|
160
|
+
platformAssignScope: 'self',
|
|
161
|
+
})
|
|
162
|
+
console.log(JSON.stringify({
|
|
163
|
+
toolNames: built.tools.map((entry) => entry.name).sort(),
|
|
164
|
+
}))
|
|
165
|
+
`)
|
|
166
|
+
|
|
167
|
+
assert.equal(output.toolNames.includes('manage_schedules'), true)
|
|
168
|
+
assert.equal(output.toolNames.includes('manage_platform'), false)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
@@ -15,15 +15,24 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
15
15
|
const normalized = normalizeToolInputArgs(args)
|
|
16
16
|
const action = normalized.action
|
|
17
17
|
const approved = normalized.approved
|
|
18
|
-
const
|
|
18
|
+
const explicitPluginId = typeof normalized.pluginId === 'string'
|
|
19
19
|
? normalized.pluginId.trim()
|
|
20
20
|
: typeof normalized.plugin_id === 'string'
|
|
21
21
|
? normalized.plugin_id.trim()
|
|
22
|
-
:
|
|
22
|
+
: typeof normalized.toolId === 'string'
|
|
23
|
+
? normalized.toolId.trim()
|
|
24
|
+
: typeof normalized.tool_id === 'string'
|
|
25
|
+
? normalized.tool_id.trim()
|
|
26
|
+
: typeof normalized.tool === 'string'
|
|
27
|
+
? normalized.tool.trim()
|
|
28
|
+
: typeof normalized.name === 'string'
|
|
29
|
+
? normalized.name.trim()
|
|
30
|
+
: undefined
|
|
23
31
|
const url = typeof normalized.url === 'string' ? normalized.url.trim() : undefined
|
|
24
32
|
const reason = normalized.reason as string | undefined
|
|
25
33
|
const manager = getPluginManager()
|
|
26
34
|
const q = typeof normalized.query === 'string' ? normalized.query : ''
|
|
35
|
+
const pluginId = explicitPluginId || (action === 'request_access' ? q.trim() : '')
|
|
27
36
|
|
|
28
37
|
console.log('[discovery] Executing action:', action, { query: q, pluginId })
|
|
29
38
|
|
|
@@ -88,7 +97,8 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
88
97
|
if (bctx?.ctx?.sessionId) {
|
|
89
98
|
const allSessions = loadSessions()
|
|
90
99
|
const currentSession = allSessions[bctx.ctx.sessionId]
|
|
91
|
-
|
|
100
|
+
const grantedTools = currentSession?.plugins || currentSession?.tools || []
|
|
101
|
+
if (currentSession && pluginIdMatches(grantedTools, pluginId)) {
|
|
92
102
|
return JSON.stringify({
|
|
93
103
|
alreadyGranted: true,
|
|
94
104
|
pluginId,
|
|
@@ -176,13 +186,13 @@ const DiscoveryPlugin: Plugin = {
|
|
|
176
186
|
tools: [
|
|
177
187
|
{
|
|
178
188
|
name: 'manage_capabilities',
|
|
179
|
-
description: '
|
|
189
|
+
description: 'Discover currently available tools, search marketplaces, or request access to a direct tool/plugin name with action="request_access" (for example "shell", "manage_schedules", or "delegate").',
|
|
180
190
|
parameters: {
|
|
181
191
|
type: 'object',
|
|
182
192
|
properties: {
|
|
183
193
|
action: { type: 'string', enum: ['discover', 'search_marketplace', 'request_access', 'install_request'] },
|
|
184
|
-
query: { type: 'string', description: 'Search term for marketplace' },
|
|
185
|
-
pluginId: { type: 'string', description: 'The
|
|
194
|
+
query: { type: 'string', description: 'Search term for marketplace, or the direct tool/plugin name for request_access' },
|
|
195
|
+
pluginId: { type: 'string', description: 'The exact tool/plugin name to request, such as "shell" or "manage_schedules"' },
|
|
186
196
|
url: { type: 'string', description: 'URL for new plugin install request' },
|
|
187
197
|
reason: { type: 'string', description: 'Why you need this capability' }
|
|
188
198
|
},
|
|
@@ -205,8 +215,8 @@ export function buildDiscoveryTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
205
215
|
description: DiscoveryPlugin.tools![0].description,
|
|
206
216
|
schema: z.object({
|
|
207
217
|
action: z.enum(['discover', 'search_marketplace', 'request_access', 'install_request']).describe('The discovery action to perform'),
|
|
208
|
-
query: z.string().optional().describe('The
|
|
209
|
-
pluginId: z.string().optional(),
|
|
218
|
+
query: z.string().optional().describe('The marketplace query, or the direct tool/plugin name to request access to'),
|
|
219
|
+
pluginId: z.string().optional().describe('The exact tool/plugin name to request, such as "shell" or "manage_schedules"'),
|
|
210
220
|
url: z.string().optional(),
|
|
211
221
|
reason: z.string().describe('Why you need to perform this discovery action')
|
|
212
222
|
})
|
|
@@ -38,6 +38,11 @@ describe('normalizeFileArgs', () => {
|
|
|
38
38
|
assert.equal(out.dirPath, 'docs')
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
+
it('defaults empty file payloads to a workspace listing instead of an unknown action', () => {
|
|
42
|
+
const out = normalizeFileArgs({})
|
|
43
|
+
assert.equal(out.action, 'list')
|
|
44
|
+
})
|
|
45
|
+
|
|
41
46
|
it('infers write from bulk file entries with text content', () => {
|
|
42
47
|
const out = normalizeFileArgs({
|
|
43
48
|
files: [
|