@swarmclawai/swarmclaw 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
const
|
|
1
|
+
const PLUGIN_ALIAS_GROUPS: string[][] = [
|
|
2
2
|
['shell', 'execute_command', 'process_tool', 'process'],
|
|
3
3
|
['files', 'read_file', 'write_file', 'list_files', 'copy_file', 'move_file', 'delete_file', 'send_file'],
|
|
4
4
|
['edit_file'],
|
|
5
5
|
['web', 'web_search', 'web_fetch'],
|
|
6
6
|
['browser', 'openclaw_browser'],
|
|
7
|
-
['delegate', 'claude_code', 'codex_cli', 'opencode_cli', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli'],
|
|
7
|
+
['delegate', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli', 'delegate_to_gemini_cli'],
|
|
8
8
|
['manage_platform', 'manage_agents', 'manage_tasks', 'manage_schedules', 'manage_skills', 'manage_documents', 'manage_webhooks', 'manage_secrets', 'manage_sessions'],
|
|
9
9
|
['manage_connectors', 'connectors', 'connector_message_tool'],
|
|
10
10
|
['manage_chatrooms', 'chatroom'],
|
|
@@ -20,37 +20,75 @@ const TOOL_ALIAS_GROUPS: string[][] = [
|
|
|
20
20
|
['context_mgmt', 'context_status', 'context_summarize'],
|
|
21
21
|
['openclaw_workspace'],
|
|
22
22
|
['openclaw_nodes'],
|
|
23
|
+
['image_gen', 'generate_image'],
|
|
24
|
+
['email', 'send_email'],
|
|
25
|
+
['calendar', 'calendar_events'],
|
|
26
|
+
['replicate', 'replicate_run', 'replicate_models'],
|
|
27
|
+
['mailbox', 'inbox'],
|
|
28
|
+
['ask_human', 'human_loop'],
|
|
29
|
+
['document', 'ocr_document', 'parse_document'],
|
|
30
|
+
['extract', 'extract_structured'],
|
|
31
|
+
['table', 'dataframe'],
|
|
32
|
+
['crawl', 'site_crawler'],
|
|
23
33
|
]
|
|
24
34
|
|
|
25
|
-
const
|
|
35
|
+
const PLUGIN_CANONICAL_MAP = (() => {
|
|
36
|
+
const map = new Map<string, string>()
|
|
37
|
+
for (const group of PLUGIN_ALIAS_GROUPS) {
|
|
38
|
+
const normalized = group.map((id) => id.trim().toLowerCase()).filter(Boolean)
|
|
39
|
+
const canonical = normalized[0]
|
|
40
|
+
if (!canonical) continue
|
|
41
|
+
for (const id of normalized) map.set(id, canonical)
|
|
42
|
+
}
|
|
43
|
+
return map
|
|
44
|
+
})()
|
|
45
|
+
|
|
46
|
+
const PLUGIN_ALIAS_MAP = (() => {
|
|
26
47
|
const map = new Map<string, Set<string>>()
|
|
27
|
-
for (const group of
|
|
28
|
-
const normalized = group.map((
|
|
29
|
-
for (const
|
|
30
|
-
const current = map.get(
|
|
48
|
+
for (const group of PLUGIN_ALIAS_GROUPS) {
|
|
49
|
+
const normalized = group.map((id) => id.trim().toLowerCase()).filter(Boolean)
|
|
50
|
+
for (const id of normalized) {
|
|
51
|
+
const current = map.get(id) || new Set<string>()
|
|
31
52
|
for (const alias of normalized) current.add(alias)
|
|
32
|
-
map.set(
|
|
53
|
+
map.set(id, current)
|
|
33
54
|
}
|
|
34
55
|
}
|
|
35
56
|
return map
|
|
36
57
|
})()
|
|
37
58
|
|
|
38
|
-
export function
|
|
59
|
+
export function normalizePluginId(value: unknown): string {
|
|
39
60
|
return typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
40
61
|
}
|
|
41
62
|
|
|
42
|
-
export function
|
|
63
|
+
export function canonicalizePluginId(value: unknown): string {
|
|
64
|
+
const raw = typeof value === 'string' ? value.trim() : ''
|
|
65
|
+
const normalized = normalizePluginId(value)
|
|
66
|
+
if (!normalized) return raw
|
|
67
|
+
return PLUGIN_CANONICAL_MAP.get(normalized) || raw
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getPluginAliases(value: unknown): string[] {
|
|
71
|
+
const normalized = normalizePluginId(value)
|
|
72
|
+
if (!normalized) return []
|
|
73
|
+
const aliases = PLUGIN_ALIAS_MAP.get(normalized)
|
|
74
|
+
if (!aliases) return [normalized]
|
|
75
|
+
return Array.from(aliases)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function expandPluginIds(values: string[] | null | undefined): string[] {
|
|
43
79
|
if (!Array.isArray(values) || values.length === 0) return []
|
|
44
80
|
const expanded = new Set<string>()
|
|
45
81
|
const queue: string[] = values
|
|
46
|
-
.map((
|
|
82
|
+
.map((id) => typeof id === 'string' ? id.trim() : '')
|
|
47
83
|
.filter(Boolean)
|
|
48
84
|
|
|
49
85
|
while (queue.length > 0) {
|
|
50
86
|
const next = queue.shift()!
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
87
|
+
const normalized = normalizePluginId(next)
|
|
88
|
+
const aliases = PLUGIN_ALIAS_MAP.get(normalized)
|
|
89
|
+
const key = aliases ? normalized : next
|
|
90
|
+
if (expanded.has(key)) continue
|
|
91
|
+
expanded.add(key)
|
|
54
92
|
if (!aliases) continue
|
|
55
93
|
for (const alias of aliases) {
|
|
56
94
|
if (!expanded.has(alias)) queue.push(alias)
|
|
@@ -60,9 +98,19 @@ export function expandToolIds(values: string[] | null | undefined): string[] {
|
|
|
60
98
|
return Array.from(expanded)
|
|
61
99
|
}
|
|
62
100
|
|
|
63
|
-
export function
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
export function pluginIdMatches(enabledPlugins: string[] | null | undefined, pluginId: string): boolean {
|
|
102
|
+
const raw = typeof pluginId === 'string' ? pluginId.trim() : ''
|
|
103
|
+
const normalized = normalizePluginId(pluginId)
|
|
104
|
+
if (!normalized && !raw) return false
|
|
105
|
+
const expanded = expandPluginIds(enabledPlugins)
|
|
106
|
+
return expanded.includes(raw) || expanded.includes(normalized) || expanded.includes(canonicalizePluginId(pluginId))
|
|
67
107
|
}
|
|
68
108
|
|
|
109
|
+
/** @deprecated Use normalizePluginId */
|
|
110
|
+
export const normalizeToolId = normalizePluginId
|
|
111
|
+
/** @deprecated Use canonicalizePluginId */
|
|
112
|
+
export const canonicalizeToolId = canonicalizePluginId
|
|
113
|
+
/** @deprecated Use expandPluginIds */
|
|
114
|
+
export const expandToolIds = expandPluginIds
|
|
115
|
+
/** @deprecated Use pluginIdMatches */
|
|
116
|
+
export const toolIdMatches = pluginIdMatches
|
|
@@ -7,15 +7,15 @@ import {
|
|
|
7
7
|
|
|
8
8
|
test('capability policy permissive mode allows non-blocked tools', () => {
|
|
9
9
|
const decision = resolveSessionToolPolicy(['shell', 'web_search'], { capabilityPolicyMode: 'permissive' })
|
|
10
|
-
assert.deepEqual(decision.
|
|
11
|
-
assert.equal(decision.
|
|
10
|
+
assert.deepEqual(decision.enabledPlugins, ['shell', 'web_search'])
|
|
11
|
+
assert.equal(decision.blockedPlugins.length, 0)
|
|
12
12
|
})
|
|
13
13
|
|
|
14
14
|
test('capability policy balanced mode blocks destructive delete_file', () => {
|
|
15
15
|
const decision = resolveSessionToolPolicy(['files', 'delete_file'], { capabilityPolicyMode: 'balanced' })
|
|
16
|
-
assert.deepEqual(decision.
|
|
17
|
-
assert.equal(decision.
|
|
18
|
-
assert.equal(decision.
|
|
16
|
+
assert.deepEqual(decision.enabledPlugins, ['files'])
|
|
17
|
+
assert.equal(decision.blockedPlugins.length, 1)
|
|
18
|
+
assert.equal(decision.blockedPlugins[0].tool, 'delete_file')
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
test('capability policy strict mode blocks execution/platform families', () => {
|
|
@@ -23,9 +23,9 @@ test('capability policy strict mode blocks execution/platform families', () => {
|
|
|
23
23
|
['shell', 'manage_tasks', 'web_search', 'memory'],
|
|
24
24
|
{ capabilityPolicyMode: 'strict' },
|
|
25
25
|
)
|
|
26
|
-
assert.deepEqual(decision.
|
|
27
|
-
assert.equal(decision.
|
|
28
|
-
assert.equal(decision.
|
|
26
|
+
assert.deepEqual(decision.enabledPlugins, ['web_search', 'memory'])
|
|
27
|
+
assert.equal(decision.blockedPlugins.some((entry) => entry.tool === 'shell'), true)
|
|
28
|
+
assert.equal(decision.blockedPlugins.some((entry) => entry.tool === 'manage_tasks'), true)
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
test('capability policy respects explicit allow overrides', () => {
|
|
@@ -36,7 +36,7 @@ test('capability policy respects explicit allow overrides', () => {
|
|
|
36
36
|
capabilityAllowedTools: ['shell'],
|
|
37
37
|
},
|
|
38
38
|
)
|
|
39
|
-
assert.deepEqual(decision.
|
|
39
|
+
assert.deepEqual(decision.enabledPlugins, ['shell', 'web_search'])
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
test('concrete tool checks inherit blocked family rules', () => {
|
|
@@ -8,13 +8,16 @@ export interface CapabilityPolicyBlock {
|
|
|
8
8
|
source: 'safety' | 'policy'
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export interface
|
|
11
|
+
export interface PluginPolicyDecision {
|
|
12
12
|
mode: CapabilityPolicyMode
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
requestedPlugins: string[]
|
|
14
|
+
enabledPlugins: string[]
|
|
15
|
+
blockedPlugins: CapabilityPolicyBlock[]
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** @deprecated Use PluginPolicyDecision */
|
|
19
|
+
export type SessionToolPolicyDecision = PluginPolicyDecision
|
|
20
|
+
|
|
18
21
|
type CapabilityCategory =
|
|
19
22
|
| 'filesystem'
|
|
20
23
|
| 'execution'
|
|
@@ -47,10 +50,11 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
|
|
|
47
50
|
web_search: { categories: ['network'], concreteTools: ['web_search'] },
|
|
48
51
|
web_fetch: { categories: ['network'], concreteTools: ['web_fetch'] },
|
|
49
52
|
browser: { categories: ['browser', 'network'], concreteTools: ['browser', 'openclaw_browser'] },
|
|
50
|
-
delegate: { categories: ['delegation', 'execution'], concreteTools: ['delegate', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli'] },
|
|
53
|
+
delegate: { categories: ['delegation', 'execution'], concreteTools: ['delegate', 'delegate_to_claude_code', 'delegate_to_codex_cli', 'delegate_to_opencode_cli', 'delegate_to_gemini_cli'] },
|
|
51
54
|
claude_code: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_claude_code'] },
|
|
52
55
|
codex_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_codex_cli'] },
|
|
53
56
|
opencode_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_opencode_cli'] },
|
|
57
|
+
gemini_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_gemini_cli'] },
|
|
54
58
|
memory: { categories: ['memory'], concreteTools: ['memory', 'memory_tool', 'context_status', 'context_summarize'] },
|
|
55
59
|
sandbox: { categories: ['execution', 'filesystem'], concreteTools: ['sandbox', 'sandbox_exec', 'sandbox_list_runtimes', 'openclaw_sandbox'] },
|
|
56
60
|
git: { categories: ['execution', 'filesystem'], concreteTools: ['git'] },
|
|
@@ -78,6 +82,12 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
|
|
|
78
82
|
context_mgmt: { categories: ['memory'], concreteTools: ['context_mgmt', 'context_status', 'context_summarize'] },
|
|
79
83
|
plugin_creator: { categories: ['filesystem', 'execution'], concreteTools: ['plugin_creator', 'plugin_creator_tool'] },
|
|
80
84
|
sample_ui: { categories: ['platform'], concreteTools: ['sample_ui', 'show_plugin_card'] },
|
|
85
|
+
mailbox: { categories: ['network', 'platform', 'outbound'], concreteTools: ['mailbox', 'inbox'] },
|
|
86
|
+
ask_human: { categories: ['platform'], concreteTools: ['ask_human', 'human_loop'] },
|
|
87
|
+
document: { categories: ['filesystem', 'platform'], concreteTools: ['document', 'ocr_document', 'parse_document'] },
|
|
88
|
+
extract: { categories: ['filesystem', 'network'], concreteTools: ['extract', 'extract_structured'] },
|
|
89
|
+
table: { categories: ['filesystem'], concreteTools: ['table', 'dataframe'] },
|
|
90
|
+
crawl: { categories: ['network'], concreteTools: ['crawl', 'site_crawler'] },
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
const CONCRETE_TOOL_TO_SESSION_TOOL = new Map<string, string>()
|
|
@@ -144,6 +154,7 @@ function safetyMatchesTool(safetyBlocked: Set<string>, toolName: string, descrip
|
|
|
144
154
|
if (toolName === 'claude_code' && safetyBlocked.has('delegate_to_claude_code')) return true
|
|
145
155
|
if (toolName === 'codex_cli' && safetyBlocked.has('delegate_to_codex_cli')) return true
|
|
146
156
|
if (toolName === 'opencode_cli' && safetyBlocked.has('delegate_to_opencode_cli')) return true
|
|
157
|
+
if (toolName === 'gemini_cli' && safetyBlocked.has('delegate_to_gemini_cli')) return true
|
|
147
158
|
return false
|
|
148
159
|
}
|
|
149
160
|
|
|
@@ -196,51 +207,51 @@ export function resolveSessionToolPolicy(
|
|
|
196
207
|
blockedCategories,
|
|
197
208
|
} = parsePolicyConfig(normalizedSettings)
|
|
198
209
|
|
|
199
|
-
const
|
|
200
|
-
? Array.from(new Set(sessionTools.map((
|
|
210
|
+
const requestedPlugins = Array.isArray(sessionTools)
|
|
211
|
+
? Array.from(new Set(sessionTools.map((id) => normalizeName(id)).filter(Boolean)))
|
|
201
212
|
: []
|
|
202
213
|
|
|
203
|
-
const
|
|
204
|
-
const
|
|
214
|
+
const enabledPlugins: string[] = []
|
|
215
|
+
const blockedPlugins: CapabilityPolicyBlock[] = []
|
|
205
216
|
|
|
206
|
-
for (const
|
|
207
|
-
const descriptor = TOOL_DESCRIPTORS[
|
|
217
|
+
for (const pluginName of requestedPlugins) {
|
|
218
|
+
const descriptor = TOOL_DESCRIPTORS[pluginName]
|
|
208
219
|
|
|
209
|
-
if (safetyMatchesTool(safetyBlocked,
|
|
210
|
-
|
|
220
|
+
if (safetyMatchesTool(safetyBlocked, pluginName, descriptor)) {
|
|
221
|
+
blockedPlugins.push({ tool: pluginName, reason: 'blocked by safety policy', source: 'safety' })
|
|
211
222
|
continue
|
|
212
223
|
}
|
|
213
224
|
|
|
214
|
-
if (policyAllowedNames.has(
|
|
215
|
-
|
|
225
|
+
if (policyAllowedNames.has(pluginName)) {
|
|
226
|
+
enabledPlugins.push(pluginName)
|
|
216
227
|
continue
|
|
217
228
|
}
|
|
218
229
|
|
|
219
|
-
if (policyMatchesTool(policyBlockedNames,
|
|
220
|
-
|
|
230
|
+
if (policyMatchesTool(policyBlockedNames, pluginName, descriptor)) {
|
|
231
|
+
blockedPlugins.push({ tool: pluginName, reason: 'blocked by explicit policy rule', source: 'policy' })
|
|
221
232
|
continue
|
|
222
233
|
}
|
|
223
234
|
|
|
224
235
|
const categoryReason = categoryBlockReason(blockedCategories, descriptor)
|
|
225
236
|
if (categoryReason) {
|
|
226
|
-
|
|
237
|
+
blockedPlugins.push({ tool: pluginName, reason: categoryReason, source: 'policy' })
|
|
227
238
|
continue
|
|
228
239
|
}
|
|
229
240
|
|
|
230
|
-
const modeReason = modeBlocksTool(mode,
|
|
241
|
+
const modeReason = modeBlocksTool(mode, pluginName, descriptor)
|
|
231
242
|
if (modeReason) {
|
|
232
|
-
|
|
243
|
+
blockedPlugins.push({ tool: pluginName, reason: modeReason, source: 'policy' })
|
|
233
244
|
continue
|
|
234
245
|
}
|
|
235
246
|
|
|
236
|
-
|
|
247
|
+
enabledPlugins.push(pluginName)
|
|
237
248
|
}
|
|
238
249
|
|
|
239
250
|
return {
|
|
240
251
|
mode,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
252
|
+
requestedPlugins,
|
|
253
|
+
enabledPlugins,
|
|
254
|
+
blockedPlugins,
|
|
244
255
|
}
|
|
245
256
|
}
|
|
246
257
|
|
|
@@ -270,11 +281,11 @@ export function resolveConcreteToolPolicyBlock(
|
|
|
270
281
|
}
|
|
271
282
|
|
|
272
283
|
if (mappedTool) {
|
|
273
|
-
const blockedRoot = decision.
|
|
284
|
+
const blockedRoot = decision.blockedPlugins.find((entry) => entry.tool === mappedTool)
|
|
274
285
|
if (blockedRoot) return blockedRoot.reason
|
|
275
286
|
|
|
276
|
-
const enabledRoot = decision.
|
|
277
|
-
if (!enabledRoot) return `
|
|
287
|
+
const enabledRoot = decision.enabledPlugins.includes(mappedTool)
|
|
288
|
+
if (!enabledRoot) return `plugin family "${mappedTool}" is not enabled for this chat`
|
|
278
289
|
}
|
|
279
290
|
|
|
280
291
|
return null
|
|
@@ -6,6 +6,7 @@ export interface RetryOptions {
|
|
|
6
6
|
maxAttempts?: number
|
|
7
7
|
backoffMs?: number
|
|
8
8
|
retryable?: RegExp[]
|
|
9
|
+
onRetry?: (attempt: number, lastResult: string) => Promise<void> | void
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
const DEFAULT_RETRYABLE: RegExp[] = [
|
|
@@ -49,6 +50,7 @@ export async function withRetry<TArgs>(
|
|
|
49
50
|
|
|
50
51
|
// Only retry if the result looks like a retryable error
|
|
51
52
|
if (attempt < maxAttempts && isRetryableError(lastResult, retryable)) {
|
|
53
|
+
await opts?.onRetry?.(attempt, lastResult)
|
|
52
54
|
const delay = backoffMs * Math.pow(2, attempt - 1)
|
|
53
55
|
console.warn(
|
|
54
56
|
`[tool-retry] Attempt ${attempt}/${maxAttempts} matched retryable pattern, retrying in ${delay}ms`,
|
|
@@ -0,0 +1,173 @@
|
|
|
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 { after, before, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
const originalEnv = {
|
|
8
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
9
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
10
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let tempDir = ''
|
|
14
|
+
let watchJobs: typeof import('./watch-jobs')
|
|
15
|
+
let storage: typeof import('./storage')
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-watch-jobs-'))
|
|
19
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
20
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
21
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
22
|
+
watchJobs = await import('./watch-jobs')
|
|
23
|
+
storage = await import('./storage')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
after(() => {
|
|
27
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
28
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
29
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
30
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
31
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
32
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
33
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('watch-jobs', () => {
|
|
37
|
+
it('validates required targets for durable watches', async () => {
|
|
38
|
+
await assert.rejects(
|
|
39
|
+
watchJobs.createWatchJob({
|
|
40
|
+
type: 'http',
|
|
41
|
+
resumeMessage: 'resume',
|
|
42
|
+
target: {},
|
|
43
|
+
condition: {},
|
|
44
|
+
}),
|
|
45
|
+
/url target/,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
await assert.rejects(
|
|
49
|
+
watchJobs.createWatchJob({
|
|
50
|
+
type: 'time',
|
|
51
|
+
resumeMessage: 'resume',
|
|
52
|
+
target: { source: 'test' },
|
|
53
|
+
condition: {},
|
|
54
|
+
}),
|
|
55
|
+
/runAt or delayMinutes/,
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('triggers time and task watches durably', async () => {
|
|
60
|
+
const tasks = storage.loadTasks()
|
|
61
|
+
tasks.task_done = {
|
|
62
|
+
id: 'task_done',
|
|
63
|
+
title: 'done',
|
|
64
|
+
status: 'completed',
|
|
65
|
+
result: 'ok',
|
|
66
|
+
createdAt: Date.now(),
|
|
67
|
+
updatedAt: Date.now(),
|
|
68
|
+
}
|
|
69
|
+
storage.saveTasks(tasks)
|
|
70
|
+
|
|
71
|
+
const timeJob = await watchJobs.createWatchJob({
|
|
72
|
+
type: 'time',
|
|
73
|
+
resumeMessage: 'wake up',
|
|
74
|
+
target: { source: 'schedule_wake' },
|
|
75
|
+
condition: {},
|
|
76
|
+
runAt: Date.now() - 1000,
|
|
77
|
+
})
|
|
78
|
+
const taskJob = await watchJobs.createWatchJob({
|
|
79
|
+
type: 'task',
|
|
80
|
+
resumeMessage: 'task finished',
|
|
81
|
+
target: { taskId: 'task_done' },
|
|
82
|
+
condition: { statusIn: ['completed'] },
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const outcome = await watchJobs.processDueWatchJobs(Date.now())
|
|
86
|
+
|
|
87
|
+
assert.equal(outcome.triggered >= 2, true)
|
|
88
|
+
assert.equal(watchJobs.getWatchJob(timeJob.id)?.status, 'triggered')
|
|
89
|
+
assert.equal(watchJobs.getWatchJob(taskJob.id)?.status, 'triggered')
|
|
90
|
+
assert.equal(watchJobs.getWatchJob(taskJob.id)?.result?.status, 'completed')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('captures file changes and webhook triggers', async () => {
|
|
94
|
+
const watchedFile = path.join(tempDir, 'watch.txt')
|
|
95
|
+
fs.writeFileSync(watchedFile, 'alpha')
|
|
96
|
+
|
|
97
|
+
const fileJob = await watchJobs.createWatchJob({
|
|
98
|
+
type: 'file',
|
|
99
|
+
resumeMessage: 'file changed',
|
|
100
|
+
target: { path: watchedFile },
|
|
101
|
+
condition: { changed: true },
|
|
102
|
+
})
|
|
103
|
+
const webhookJob = await watchJobs.createWatchJob({
|
|
104
|
+
type: 'webhook',
|
|
105
|
+
resumeMessage: 'webhook arrived',
|
|
106
|
+
target: { webhookId: 'wh_1' },
|
|
107
|
+
condition: { event: 'build.finished' },
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
fs.writeFileSync(watchedFile, 'beta')
|
|
111
|
+
await watchJobs.processDueWatchJobs(Date.now())
|
|
112
|
+
const webhookMatches = watchJobs.triggerWebhookWatchJobs({
|
|
113
|
+
webhookId: 'wh_1',
|
|
114
|
+
event: 'build.finished',
|
|
115
|
+
payloadPreview: '{"ok":true}',
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
assert.equal(watchJobs.getWatchJob(fileJob.id)?.status, 'triggered')
|
|
119
|
+
assert.match(String(watchJobs.getWatchJob(fileJob.id)?.result?.preview || ''), /beta/)
|
|
120
|
+
assert.equal(webhookMatches.length, 1)
|
|
121
|
+
assert.equal(watchJobs.getWatchJob(webhookJob.id)?.status, 'triggered')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('wakes mailbox and approval watches from event triggers', async () => {
|
|
125
|
+
storage.upsertApproval('approval_1', {
|
|
126
|
+
id: 'approval_1',
|
|
127
|
+
category: 'human_loop',
|
|
128
|
+
title: 'Need approval',
|
|
129
|
+
description: 'Approve the action',
|
|
130
|
+
data: {},
|
|
131
|
+
createdAt: Date.now(),
|
|
132
|
+
updatedAt: Date.now(),
|
|
133
|
+
status: 'pending',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const mailboxJob = await watchJobs.createWatchJob({
|
|
137
|
+
type: 'mailbox',
|
|
138
|
+
resumeMessage: 'human replied',
|
|
139
|
+
target: { sessionId: 'session_1' },
|
|
140
|
+
condition: { type: 'human_reply', correlationId: 'corr_1' },
|
|
141
|
+
})
|
|
142
|
+
const approvalJob = await watchJobs.createWatchJob({
|
|
143
|
+
type: 'approval',
|
|
144
|
+
resumeMessage: 'approval updated',
|
|
145
|
+
target: { approvalId: 'approval_1' },
|
|
146
|
+
condition: { statusIn: ['approved'] },
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const mailboxMatches = watchJobs.triggerMailboxWatchJobs({
|
|
150
|
+
sessionId: 'session_1',
|
|
151
|
+
envelope: {
|
|
152
|
+
id: 'env_1',
|
|
153
|
+
type: 'human_reply',
|
|
154
|
+
payload: 'approved',
|
|
155
|
+
toSessionId: 'session_1',
|
|
156
|
+
correlationId: 'corr_1',
|
|
157
|
+
status: 'new',
|
|
158
|
+
createdAt: Date.now(),
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
const approvalMatches = watchJobs.triggerApprovalWatchJobs({
|
|
162
|
+
approvalId: 'approval_1',
|
|
163
|
+
status: 'approved',
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
assert.equal(mailboxMatches.length, 1)
|
|
167
|
+
assert.equal(approvalMatches.length, 1)
|
|
168
|
+
assert.equal(watchJobs.getWatchJob(mailboxJob.id)?.status, 'triggered')
|
|
169
|
+
assert.equal(watchJobs.getWatchJob(approvalJob.id)?.status, 'triggered')
|
|
170
|
+
assert.equal(watchJobs.getWatchJob(mailboxJob.id)?.result?.correlationId, 'corr_1')
|
|
171
|
+
assert.equal(watchJobs.getWatchJob(approvalJob.id)?.result?.status, 'approved')
|
|
172
|
+
})
|
|
173
|
+
})
|