@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.
- 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 +3 -1
- 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/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/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 +10 -4
- 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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { api } from './api-client'
|
|
2
|
+
import type { ProviderModelDiscoveryResult } from '@/types'
|
|
3
|
+
|
|
4
|
+
export interface DiscoverProviderModelsParams {
|
|
5
|
+
providerId: string
|
|
6
|
+
credentialId?: string | null
|
|
7
|
+
endpoint?: string | null
|
|
8
|
+
force?: boolean
|
|
9
|
+
requiresApiKey?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildProviderModelDiscoveryPath(params: DiscoverProviderModelsParams): string {
|
|
13
|
+
const searchParams = new URLSearchParams()
|
|
14
|
+
if (params.credentialId) searchParams.set('credentialId', params.credentialId)
|
|
15
|
+
if (params.endpoint?.trim()) searchParams.set('endpoint', params.endpoint.trim())
|
|
16
|
+
if (params.force) searchParams.set('force', '1')
|
|
17
|
+
if (typeof params.requiresApiKey === 'boolean') {
|
|
18
|
+
searchParams.set('requiresApiKey', params.requiresApiKey ? '1' : '0')
|
|
19
|
+
}
|
|
20
|
+
const query = searchParams.toString()
|
|
21
|
+
const encodedProviderId = encodeURIComponent(params.providerId)
|
|
22
|
+
return `/providers/${encodedProviderId}/discover-models${query ? `?${query}` : ''}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function fetchProviderModelDiscovery(
|
|
26
|
+
params: DiscoverProviderModelsParams,
|
|
27
|
+
): Promise<ProviderModelDiscoveryResult> {
|
|
28
|
+
return api<ProviderModelDiscoveryResult>('GET', buildProviderModelDiscoveryPath(params))
|
|
29
|
+
}
|
|
@@ -256,11 +256,16 @@ export function getProviderList(): ProviderInfo[] {
|
|
|
256
256
|
const overrides = getModelOverrides()
|
|
257
257
|
const builtins = Object.values(PROVIDERS)
|
|
258
258
|
.filter(({ id }) => id !== 'openclaw')
|
|
259
|
-
.map((
|
|
260
|
-
...info
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
259
|
+
.map((provider) => {
|
|
260
|
+
const { handler, ...info } = provider
|
|
261
|
+
void handler
|
|
262
|
+
return {
|
|
263
|
+
...info,
|
|
264
|
+
models: overrides[info.id] || info.models,
|
|
265
|
+
defaultModels: info.models,
|
|
266
|
+
supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'fireworks'].includes(info.id),
|
|
267
|
+
}
|
|
268
|
+
})
|
|
264
269
|
|
|
265
270
|
const customs: ProviderInfo[] = Object.values(getCustomProviders())
|
|
266
271
|
.filter((c) => c.isEnabled)
|
|
@@ -269,6 +274,7 @@ export function getProviderList(): ProviderInfo[] {
|
|
|
269
274
|
name: c.name,
|
|
270
275
|
models: c.models,
|
|
271
276
|
defaultModels: c.models,
|
|
277
|
+
supportsModelDiscovery: !!(c.baseUrl && c.baseUrl.trim()),
|
|
272
278
|
requiresApiKey: c.requiresApiKey,
|
|
273
279
|
requiresEndpoint: false as boolean,
|
|
274
280
|
defaultEndpoint: c.baseUrl,
|
|
@@ -283,6 +289,7 @@ export function getProviderList(): ProviderInfo[] {
|
|
|
283
289
|
name: String(p.name),
|
|
284
290
|
models: p.models as string[],
|
|
285
291
|
defaultModels: p.models as string[],
|
|
292
|
+
supportsModelDiscovery: Boolean(p.supportsModelDiscovery),
|
|
286
293
|
requiresApiKey: Boolean(p.requiresApiKey),
|
|
287
294
|
requiresEndpoint: Boolean(p.requiresEndpoint),
|
|
288
295
|
defaultEndpoint: p.defaultEndpoint as string | undefined,
|
package/src/lib/runtime-loop.ts
CHANGED
|
@@ -3,9 +3,28 @@ import type { LoopMode } from '@/types'
|
|
|
3
3
|
export const DEFAULT_LOOP_MODE: LoopMode = 'bounded'
|
|
4
4
|
|
|
5
5
|
// Loop limits
|
|
6
|
-
export const
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
6
|
+
export const AGENT_LOOP_RECURSION_LIMIT_MIN = 1
|
|
7
|
+
export const AGENT_LOOP_RECURSION_LIMIT_MAX = 200
|
|
8
|
+
export const ORCHESTRATOR_LOOP_RECURSION_LIMIT_MIN = 1
|
|
9
|
+
export const ORCHESTRATOR_LOOP_RECURSION_LIMIT_MAX = 300
|
|
10
|
+
export const LEGACY_ORCHESTRATOR_MAX_TURNS_MIN = 1
|
|
11
|
+
export const LEGACY_ORCHESTRATOR_MAX_TURNS_MAX = 300
|
|
12
|
+
export const ONGOING_LOOP_MAX_ITERATIONS_MIN = 10
|
|
13
|
+
export const ONGOING_LOOP_MAX_ITERATIONS_MAX = 5000
|
|
14
|
+
export const ONGOING_LOOP_MAX_RUNTIME_MINUTES_MIN = 0
|
|
15
|
+
export const ONGOING_LOOP_MAX_RUNTIME_MINUTES_MAX = 1440
|
|
16
|
+
export const DELEGATION_MAX_DEPTH_MIN = 1
|
|
17
|
+
export const DELEGATION_MAX_DEPTH_MAX = 12
|
|
18
|
+
export const SHELL_COMMAND_TIMEOUT_SEC_MIN = 1
|
|
19
|
+
export const SHELL_COMMAND_TIMEOUT_SEC_MAX = 600
|
|
20
|
+
export const CLAUDE_CODE_TIMEOUT_SEC_MIN = 5
|
|
21
|
+
export const CLAUDE_CODE_TIMEOUT_SEC_MAX = 7200
|
|
22
|
+
export const CLI_PROCESS_TIMEOUT_SEC_MIN = 10
|
|
23
|
+
export const CLI_PROCESS_TIMEOUT_SEC_MAX = 7200
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_AGENT_LOOP_RECURSION_LIMIT = 60
|
|
26
|
+
export const DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT = 80
|
|
27
|
+
export const DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS = 16
|
|
9
28
|
export const DEFAULT_ONGOING_LOOP_MAX_ITERATIONS = 250
|
|
10
29
|
export const DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES = 60
|
|
11
30
|
export const DEFAULT_DELEGATION_MAX_DEPTH = 3
|
|
@@ -14,3 +33,86 @@ export const DEFAULT_DELEGATION_MAX_DEPTH = 3
|
|
|
14
33
|
export const DEFAULT_SHELL_COMMAND_TIMEOUT_SEC = 30
|
|
15
34
|
export const DEFAULT_CLAUDE_CODE_TIMEOUT_SEC = 1800
|
|
16
35
|
export const DEFAULT_CLI_PROCESS_TIMEOUT_SEC = 1800
|
|
36
|
+
|
|
37
|
+
function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
|
|
38
|
+
const parsed = typeof value === 'number'
|
|
39
|
+
? value
|
|
40
|
+
: typeof value === 'string'
|
|
41
|
+
? Number.parseInt(value, 10)
|
|
42
|
+
: Number.NaN
|
|
43
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
44
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface NormalizedRuntimeSettingFields {
|
|
48
|
+
loopMode: LoopMode
|
|
49
|
+
agentLoopRecursionLimit: number
|
|
50
|
+
orchestratorLoopRecursionLimit: number
|
|
51
|
+
legacyOrchestratorMaxTurns: number
|
|
52
|
+
delegationMaxDepth: number
|
|
53
|
+
ongoingLoopMaxIterations: number
|
|
54
|
+
ongoingLoopMaxRuntimeMinutes: number
|
|
55
|
+
shellCommandTimeoutSec: number
|
|
56
|
+
claudeCodeTimeoutSec: number
|
|
57
|
+
cliProcessTimeoutSec: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeRuntimeSettingFields(settings: Record<string, unknown>): NormalizedRuntimeSettingFields {
|
|
61
|
+
return {
|
|
62
|
+
loopMode: settings.loopMode === 'ongoing' ? 'ongoing' : DEFAULT_LOOP_MODE,
|
|
63
|
+
agentLoopRecursionLimit: parseIntSetting(
|
|
64
|
+
settings.agentLoopRecursionLimit,
|
|
65
|
+
DEFAULT_AGENT_LOOP_RECURSION_LIMIT,
|
|
66
|
+
AGENT_LOOP_RECURSION_LIMIT_MIN,
|
|
67
|
+
AGENT_LOOP_RECURSION_LIMIT_MAX,
|
|
68
|
+
),
|
|
69
|
+
orchestratorLoopRecursionLimit: parseIntSetting(
|
|
70
|
+
settings.orchestratorLoopRecursionLimit,
|
|
71
|
+
DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT,
|
|
72
|
+
ORCHESTRATOR_LOOP_RECURSION_LIMIT_MIN,
|
|
73
|
+
ORCHESTRATOR_LOOP_RECURSION_LIMIT_MAX,
|
|
74
|
+
),
|
|
75
|
+
legacyOrchestratorMaxTurns: parseIntSetting(
|
|
76
|
+
settings.legacyOrchestratorMaxTurns,
|
|
77
|
+
DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS,
|
|
78
|
+
LEGACY_ORCHESTRATOR_MAX_TURNS_MIN,
|
|
79
|
+
LEGACY_ORCHESTRATOR_MAX_TURNS_MAX,
|
|
80
|
+
),
|
|
81
|
+
delegationMaxDepth: parseIntSetting(
|
|
82
|
+
settings.delegationMaxDepth,
|
|
83
|
+
DEFAULT_DELEGATION_MAX_DEPTH,
|
|
84
|
+
DELEGATION_MAX_DEPTH_MIN,
|
|
85
|
+
DELEGATION_MAX_DEPTH_MAX,
|
|
86
|
+
),
|
|
87
|
+
ongoingLoopMaxIterations: parseIntSetting(
|
|
88
|
+
settings.ongoingLoopMaxIterations,
|
|
89
|
+
DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
|
|
90
|
+
ONGOING_LOOP_MAX_ITERATIONS_MIN,
|
|
91
|
+
ONGOING_LOOP_MAX_ITERATIONS_MAX,
|
|
92
|
+
),
|
|
93
|
+
ongoingLoopMaxRuntimeMinutes: parseIntSetting(
|
|
94
|
+
settings.ongoingLoopMaxRuntimeMinutes,
|
|
95
|
+
DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES,
|
|
96
|
+
ONGOING_LOOP_MAX_RUNTIME_MINUTES_MIN,
|
|
97
|
+
ONGOING_LOOP_MAX_RUNTIME_MINUTES_MAX,
|
|
98
|
+
),
|
|
99
|
+
shellCommandTimeoutSec: parseIntSetting(
|
|
100
|
+
settings.shellCommandTimeoutSec,
|
|
101
|
+
DEFAULT_SHELL_COMMAND_TIMEOUT_SEC,
|
|
102
|
+
SHELL_COMMAND_TIMEOUT_SEC_MIN,
|
|
103
|
+
SHELL_COMMAND_TIMEOUT_SEC_MAX,
|
|
104
|
+
),
|
|
105
|
+
claudeCodeTimeoutSec: parseIntSetting(
|
|
106
|
+
settings.claudeCodeTimeoutSec,
|
|
107
|
+
DEFAULT_CLAUDE_CODE_TIMEOUT_SEC,
|
|
108
|
+
CLAUDE_CODE_TIMEOUT_SEC_MIN,
|
|
109
|
+
CLAUDE_CODE_TIMEOUT_SEC_MAX,
|
|
110
|
+
),
|
|
111
|
+
cliProcessTimeoutSec: parseIntSetting(
|
|
112
|
+
settings.cliProcessTimeoutSec,
|
|
113
|
+
DEFAULT_CLI_PROCESS_TIMEOUT_SEC,
|
|
114
|
+
CLI_PROCESS_TIMEOUT_SEC_MIN,
|
|
115
|
+
CLI_PROCESS_TIMEOUT_SEC_MAX,
|
|
116
|
+
),
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/lib/safe-storage.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
function canUseLocalStorage(): boolean {
|
|
2
|
-
|
|
2
|
+
if (typeof window === 'undefined') return false
|
|
3
|
+
try {
|
|
4
|
+
return !!window.localStorage
|
|
5
|
+
} catch {
|
|
6
|
+
return false
|
|
7
|
+
}
|
|
3
8
|
}
|
|
4
9
|
|
|
5
10
|
export function safeStorageGet(key: string): string | null {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import type { Agent, GatewayProfile } from '@/types'
|
|
4
|
+
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
5
|
+
import {
|
|
6
|
+
applyResolvedRoute,
|
|
7
|
+
resolveAgentRouteCandidatesWithProfiles,
|
|
8
|
+
} from './agent-runtime-config'
|
|
9
|
+
|
|
10
|
+
function makeGateway(overrides: Partial<GatewayProfile> = {}): GatewayProfile {
|
|
11
|
+
const now = Date.now()
|
|
12
|
+
return {
|
|
13
|
+
id: 'gateway-default',
|
|
14
|
+
name: 'Gateway Default',
|
|
15
|
+
provider: 'openclaw',
|
|
16
|
+
endpoint: 'https://gateway.example.com/v1',
|
|
17
|
+
wsUrl: 'wss://gateway.example.com',
|
|
18
|
+
credentialId: 'cred-gateway',
|
|
19
|
+
status: 'healthy',
|
|
20
|
+
tags: [],
|
|
21
|
+
isDefault: true,
|
|
22
|
+
createdAt: now,
|
|
23
|
+
updatedAt: now,
|
|
24
|
+
...overrides,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeAgent(overrides: Partial<Agent> = {}): Agent {
|
|
29
|
+
const now = Date.now()
|
|
30
|
+
return {
|
|
31
|
+
id: 'agent-1',
|
|
32
|
+
name: 'OpenClaw Ops',
|
|
33
|
+
description: '',
|
|
34
|
+
systemPrompt: '',
|
|
35
|
+
provider: 'openclaw',
|
|
36
|
+
model: '',
|
|
37
|
+
createdAt: now,
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
...overrides,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('resolveAgentRouteCandidatesWithProfiles applies the default OpenClaw gateway profile to base agents', () => {
|
|
44
|
+
const gateways = [
|
|
45
|
+
makeGateway(),
|
|
46
|
+
makeGateway({
|
|
47
|
+
id: 'gateway-secondary',
|
|
48
|
+
name: 'Gateway Secondary',
|
|
49
|
+
endpoint: 'https://secondary.example.com/v1',
|
|
50
|
+
wsUrl: 'wss://secondary.example.com',
|
|
51
|
+
credentialId: 'cred-secondary',
|
|
52
|
+
isDefault: false,
|
|
53
|
+
}),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const [route] = resolveAgentRouteCandidatesWithProfiles(makeAgent(), gateways)
|
|
57
|
+
assert.ok(route)
|
|
58
|
+
assert.equal(route.provider, 'openclaw')
|
|
59
|
+
assert.equal(route.model, 'default')
|
|
60
|
+
assert.equal(route.gatewayProfileId, 'gateway-default')
|
|
61
|
+
assert.equal(route.credentialId, 'cred-gateway')
|
|
62
|
+
assert.equal(route.apiEndpoint, normalizeProviderEndpoint('openclaw', 'https://gateway.example.com/v1'))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('resolveAgentRouteCandidatesWithProfiles respects routing strategy but deprioritizes cooling providers', () => {
|
|
66
|
+
const gateways = [
|
|
67
|
+
makeGateway({
|
|
68
|
+
id: 'gateway-economy',
|
|
69
|
+
name: 'Economy Gateway',
|
|
70
|
+
endpoint: 'https://economy.example.com/v1',
|
|
71
|
+
wsUrl: 'wss://economy.example.com',
|
|
72
|
+
credentialId: 'cred-economy',
|
|
73
|
+
isDefault: false,
|
|
74
|
+
}),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
const agent = makeAgent({
|
|
78
|
+
provider: 'openai',
|
|
79
|
+
model: 'gpt-4o',
|
|
80
|
+
gatewayProfileId: null,
|
|
81
|
+
routingStrategy: 'economy',
|
|
82
|
+
routingTargets: [
|
|
83
|
+
{
|
|
84
|
+
id: 'economy-route',
|
|
85
|
+
label: 'Economy',
|
|
86
|
+
provider: 'openclaw',
|
|
87
|
+
model: 'default',
|
|
88
|
+
gatewayProfileId: 'gateway-economy',
|
|
89
|
+
role: 'economy',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'premium-route',
|
|
93
|
+
label: 'Premium',
|
|
94
|
+
provider: 'openai',
|
|
95
|
+
model: 'gpt-5',
|
|
96
|
+
role: 'premium',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const preferred = resolveAgentRouteCandidatesWithProfiles(agent, gateways)
|
|
102
|
+
assert.equal(preferred[0]?.id, 'economy-route')
|
|
103
|
+
assert.equal(preferred[0]?.apiEndpoint, normalizeProviderEndpoint('openclaw', 'https://economy.example.com/v1'))
|
|
104
|
+
|
|
105
|
+
const cooled = resolveAgentRouteCandidatesWithProfiles(agent, gateways, undefined, (providerId) => providerId === 'openclaw')
|
|
106
|
+
assert.equal(cooled[0]?.id, 'base')
|
|
107
|
+
assert.equal(cooled[0]?.provider, 'openai')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('applyResolvedRoute copies gateway, endpoint, and fallback credentials onto a target session-like object', () => {
|
|
111
|
+
const target = {
|
|
112
|
+
provider: 'claude-cli' as const,
|
|
113
|
+
model: 'claude-sonnet-4-5',
|
|
114
|
+
credentialId: null,
|
|
115
|
+
fallbackCredentialIds: [] as string[],
|
|
116
|
+
apiEndpoint: null,
|
|
117
|
+
gatewayProfileId: null,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const next = applyResolvedRoute(target, {
|
|
121
|
+
id: 'route-1',
|
|
122
|
+
label: 'Gateway route',
|
|
123
|
+
provider: 'openclaw',
|
|
124
|
+
model: 'default',
|
|
125
|
+
credentialId: 'cred-1',
|
|
126
|
+
fallbackCredentialIds: ['cred-2', 'cred-3'],
|
|
127
|
+
apiEndpoint: 'https://gateway.example.com/v1',
|
|
128
|
+
gatewayProfileId: 'gateway-1',
|
|
129
|
+
priority: 0,
|
|
130
|
+
source: 'routing-target',
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
assert.deepEqual(next, {
|
|
134
|
+
provider: 'openclaw',
|
|
135
|
+
model: 'default',
|
|
136
|
+
credentialId: 'cred-1',
|
|
137
|
+
fallbackCredentialIds: ['cred-2', 'cred-3'],
|
|
138
|
+
apiEndpoint: 'https://gateway.example.com/v1',
|
|
139
|
+
gatewayProfileId: 'gateway-1',
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Agent,
|
|
3
|
+
AgentRoutingStrategy,
|
|
4
|
+
AgentRoutingTarget,
|
|
5
|
+
GatewayProfile,
|
|
6
|
+
ProviderType,
|
|
7
|
+
} from '@/types'
|
|
8
|
+
import { deriveOpenClawWsUrl, normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
9
|
+
import { loadGatewayProfiles } from './storage'
|
|
10
|
+
import { isProviderCoolingDown } from './provider-health'
|
|
11
|
+
|
|
12
|
+
const DEFAULT_OPENCLAW_ENDPOINT = 'http://localhost:18789/v1'
|
|
13
|
+
const DEFAULT_OPENCLAW_MODEL = 'default'
|
|
14
|
+
|
|
15
|
+
export interface ResolvedAgentRoute {
|
|
16
|
+
id: string
|
|
17
|
+
label: string
|
|
18
|
+
provider: ProviderType
|
|
19
|
+
model: string
|
|
20
|
+
credentialId?: string | null
|
|
21
|
+
fallbackCredentialIds: string[]
|
|
22
|
+
apiEndpoint?: string | null
|
|
23
|
+
gatewayProfileId?: string | null
|
|
24
|
+
role?: NonNullable<AgentRoutingTarget['role']>
|
|
25
|
+
priority: number
|
|
26
|
+
source: 'agent' | 'routing-target'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface RouteSeed {
|
|
30
|
+
id: string
|
|
31
|
+
label?: string
|
|
32
|
+
provider?: ProviderType | null
|
|
33
|
+
model?: string | null
|
|
34
|
+
credentialId?: string | null
|
|
35
|
+
fallbackCredentialIds?: string[]
|
|
36
|
+
apiEndpoint?: string | null
|
|
37
|
+
gatewayProfileId?: string | null
|
|
38
|
+
role?: AgentRoutingTarget['role']
|
|
39
|
+
priority?: number
|
|
40
|
+
source: ResolvedAgentRoute['source']
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ensureStringArray(value: unknown): string[] {
|
|
44
|
+
if (!Array.isArray(value)) return []
|
|
45
|
+
return value
|
|
46
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeGateway(raw: unknown, id: string): GatewayProfile | null {
|
|
51
|
+
if (!raw || typeof raw !== 'object') return null
|
|
52
|
+
const gateway = raw as Partial<GatewayProfile> & Record<string, unknown>
|
|
53
|
+
const endpoint = normalizeProviderEndpoint(
|
|
54
|
+
'openclaw',
|
|
55
|
+
typeof gateway.endpoint === 'string' && gateway.endpoint.trim()
|
|
56
|
+
? gateway.endpoint
|
|
57
|
+
: DEFAULT_OPENCLAW_ENDPOINT,
|
|
58
|
+
)
|
|
59
|
+
if (!endpoint) return null
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
name: typeof gateway.name === 'string' && gateway.name.trim() ? gateway.name.trim() : id,
|
|
63
|
+
provider: 'openclaw',
|
|
64
|
+
endpoint,
|
|
65
|
+
wsUrl: typeof gateway.wsUrl === 'string' && gateway.wsUrl.trim() ? gateway.wsUrl.trim() : deriveOpenClawWsUrl(endpoint),
|
|
66
|
+
credentialId: typeof gateway.credentialId === 'string' && gateway.credentialId.trim() ? gateway.credentialId.trim() : null,
|
|
67
|
+
status: gateway.status === 'healthy' || gateway.status === 'degraded' || gateway.status === 'offline' || gateway.status === 'pending' ? gateway.status : 'unknown',
|
|
68
|
+
notes: typeof gateway.notes === 'string' ? gateway.notes : null,
|
|
69
|
+
tags: ensureStringArray(gateway.tags),
|
|
70
|
+
lastError: typeof gateway.lastError === 'string' ? gateway.lastError : null,
|
|
71
|
+
lastCheckedAt: typeof gateway.lastCheckedAt === 'number' ? gateway.lastCheckedAt : null,
|
|
72
|
+
lastModelCount: typeof gateway.lastModelCount === 'number' ? gateway.lastModelCount : null,
|
|
73
|
+
discoveredHost: typeof gateway.discoveredHost === 'string' ? gateway.discoveredHost : null,
|
|
74
|
+
discoveredPort: typeof gateway.discoveredPort === 'number' ? gateway.discoveredPort : null,
|
|
75
|
+
isDefault: gateway.isDefault === true,
|
|
76
|
+
createdAt: typeof gateway.createdAt === 'number' ? gateway.createdAt : Date.now(),
|
|
77
|
+
updatedAt: typeof gateway.updatedAt === 'number' ? gateway.updatedAt : Date.now(),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findGatewayProfile(
|
|
82
|
+
gatewayProfiles: GatewayProfile[],
|
|
83
|
+
profileId?: string | null,
|
|
84
|
+
): GatewayProfile | null {
|
|
85
|
+
const id = typeof profileId === 'string' ? profileId.trim() : ''
|
|
86
|
+
if (!id) return null
|
|
87
|
+
return gatewayProfiles.find((profile) => profile.id === id) || null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getGatewayProfiles(provider: GatewayProfile['provider'] | null = null): GatewayProfile[] {
|
|
91
|
+
const all = loadGatewayProfiles()
|
|
92
|
+
return Object.entries(all)
|
|
93
|
+
.map(([id, value]) => normalizeGateway(value, id))
|
|
94
|
+
.filter((value): value is GatewayProfile => Boolean(value))
|
|
95
|
+
.filter((value) => !provider || value.provider === provider)
|
|
96
|
+
.sort((a, b) => {
|
|
97
|
+
if ((a.isDefault === true) !== (b.isDefault === true)) return a.isDefault ? -1 : 1
|
|
98
|
+
return a.name.localeCompare(b.name)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getGatewayProfile(profileId?: string | null): GatewayProfile | null {
|
|
103
|
+
return findGatewayProfile(getGatewayProfiles(), profileId)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function defaultGatewayProfile(gatewayProfiles: GatewayProfile[]): GatewayProfile | null {
|
|
107
|
+
return gatewayProfiles.find((profile) => profile.isDefault) || gatewayProfiles[0] || null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function roleWeight(strategy: AgentRoutingStrategy, role?: AgentRoutingTarget['role']): number {
|
|
111
|
+
const normalized = role || 'primary'
|
|
112
|
+
const matrix: Record<AgentRoutingStrategy, Record<string, number>> = {
|
|
113
|
+
single: { primary: 0, backup: 10, premium: 20, reasoning: 30, economy: 40 },
|
|
114
|
+
balanced: { primary: 0, premium: 4, economy: 4, reasoning: 6, backup: 12 },
|
|
115
|
+
economy: { economy: 0, primary: 10, backup: 18, premium: 28, reasoning: 36 },
|
|
116
|
+
premium: { premium: 0, reasoning: 4, primary: 10, backup: 18, economy: 28 },
|
|
117
|
+
reasoning: { reasoning: 0, premium: 4, primary: 10, backup: 18, economy: 28 },
|
|
118
|
+
}
|
|
119
|
+
return matrix[strategy][normalized] ?? 50
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function dedupeCredentialIds(primary: string | null | undefined, candidates: string[] | undefined): string[] {
|
|
123
|
+
const seen = new Set<string>()
|
|
124
|
+
const normalizedPrimary = typeof primary === 'string' && primary.trim() ? primary.trim() : null
|
|
125
|
+
const result: string[] = []
|
|
126
|
+
for (const value of ensureStringArray(candidates)) {
|
|
127
|
+
if (normalizedPrimary && value === normalizedPrimary) continue
|
|
128
|
+
if (seen.has(value)) continue
|
|
129
|
+
seen.add(value)
|
|
130
|
+
result.push(value)
|
|
131
|
+
}
|
|
132
|
+
return result
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function buildRouteFromSeed(
|
|
136
|
+
seed: RouteSeed,
|
|
137
|
+
gatewayProfiles: GatewayProfile[],
|
|
138
|
+
agentGatewayProfileId?: string | null,
|
|
139
|
+
): ResolvedAgentRoute | null {
|
|
140
|
+
const provider = (seed.provider || 'claude-cli') as ProviderType
|
|
141
|
+
let gatewayProfileId = seed.gatewayProfileId ?? null
|
|
142
|
+
if (!gatewayProfileId && provider === 'openclaw') {
|
|
143
|
+
gatewayProfileId = agentGatewayProfileId ?? defaultGatewayProfile(gatewayProfiles)?.id ?? null
|
|
144
|
+
}
|
|
145
|
+
const gatewayProfile = findGatewayProfile(gatewayProfiles, gatewayProfileId)
|
|
146
|
+
|
|
147
|
+
const providerFromGateway = gatewayProfile?.provider === 'openclaw' ? 'openclaw' : provider
|
|
148
|
+
const apiEndpoint = normalizeProviderEndpoint(
|
|
149
|
+
providerFromGateway,
|
|
150
|
+
seed.apiEndpoint ?? gatewayProfile?.endpoint ?? null,
|
|
151
|
+
)
|
|
152
|
+
const model = (seed.model || '').trim() || (providerFromGateway === 'openclaw' ? DEFAULT_OPENCLAW_MODEL : '')
|
|
153
|
+
if (!providerFromGateway || !model) return null
|
|
154
|
+
|
|
155
|
+
const credentialId = seed.credentialId ?? gatewayProfile?.credentialId ?? null
|
|
156
|
+
return {
|
|
157
|
+
id: seed.id,
|
|
158
|
+
label: seed.label?.trim() || (gatewayProfile?.name || `${providerFromGateway}:${model}`),
|
|
159
|
+
provider: providerFromGateway,
|
|
160
|
+
model,
|
|
161
|
+
credentialId,
|
|
162
|
+
fallbackCredentialIds: dedupeCredentialIds(credentialId, seed.fallbackCredentialIds),
|
|
163
|
+
apiEndpoint,
|
|
164
|
+
gatewayProfileId,
|
|
165
|
+
role: seed.role,
|
|
166
|
+
priority: typeof seed.priority === 'number' ? seed.priority : 100,
|
|
167
|
+
source: seed.source,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function dedupeRoutes(routes: ResolvedAgentRoute[]): ResolvedAgentRoute[] {
|
|
172
|
+
const seen = new Set<string>()
|
|
173
|
+
const deduped: ResolvedAgentRoute[] = []
|
|
174
|
+
for (const route of routes) {
|
|
175
|
+
const key = [
|
|
176
|
+
route.provider,
|
|
177
|
+
route.model,
|
|
178
|
+
route.credentialId || '',
|
|
179
|
+
route.apiEndpoint || '',
|
|
180
|
+
route.gatewayProfileId || '',
|
|
181
|
+
].join('::')
|
|
182
|
+
if (seen.has(key)) continue
|
|
183
|
+
seen.add(key)
|
|
184
|
+
deduped.push(route)
|
|
185
|
+
}
|
|
186
|
+
return deduped
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function resolveAgentRouteCandidates(
|
|
190
|
+
agent: Agent | null | undefined,
|
|
191
|
+
preferredStrategy?: AgentRoutingStrategy | null,
|
|
192
|
+
): ResolvedAgentRoute[] {
|
|
193
|
+
return resolveAgentRouteCandidatesWithProfiles(agent, getGatewayProfiles('openclaw'), preferredStrategy)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function resolveAgentRouteCandidatesWithProfiles(
|
|
197
|
+
agent: Agent | null | undefined,
|
|
198
|
+
gatewayProfiles: GatewayProfile[],
|
|
199
|
+
preferredStrategy?: AgentRoutingStrategy | null,
|
|
200
|
+
isCoolingDown: (providerId: string) => boolean = isProviderCoolingDown,
|
|
201
|
+
): ResolvedAgentRoute[] {
|
|
202
|
+
if (!agent) return []
|
|
203
|
+
const strategy = preferredStrategy || agent.routingStrategy || 'single'
|
|
204
|
+
const seeds: RouteSeed[] = [
|
|
205
|
+
{
|
|
206
|
+
id: 'base',
|
|
207
|
+
label: agent.name,
|
|
208
|
+
provider: agent.provider,
|
|
209
|
+
model: agent.model,
|
|
210
|
+
credentialId: agent.credentialId ?? null,
|
|
211
|
+
fallbackCredentialIds: agent.fallbackCredentialIds || [],
|
|
212
|
+
apiEndpoint: agent.apiEndpoint ?? null,
|
|
213
|
+
gatewayProfileId: agent.gatewayProfileId ?? null,
|
|
214
|
+
role: 'primary',
|
|
215
|
+
priority: 0,
|
|
216
|
+
source: 'agent',
|
|
217
|
+
},
|
|
218
|
+
...((agent.routingTargets || []).map((target, index) => ({
|
|
219
|
+
id: target.id || `route-${index + 1}`,
|
|
220
|
+
label: target.label,
|
|
221
|
+
provider: target.provider,
|
|
222
|
+
model: target.model,
|
|
223
|
+
credentialId: target.credentialId ?? null,
|
|
224
|
+
fallbackCredentialIds: target.fallbackCredentialIds || [],
|
|
225
|
+
apiEndpoint: target.apiEndpoint ?? null,
|
|
226
|
+
gatewayProfileId: target.gatewayProfileId ?? null,
|
|
227
|
+
role: target.role,
|
|
228
|
+
priority: typeof target.priority === 'number' ? target.priority : index + 1,
|
|
229
|
+
source: 'routing-target' as const,
|
|
230
|
+
}))),
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
return dedupeRoutes(
|
|
234
|
+
seeds
|
|
235
|
+
.map((seed) => buildRouteFromSeed(seed, gatewayProfiles, agent.gatewayProfileId ?? null))
|
|
236
|
+
.filter((route): route is ResolvedAgentRoute => Boolean(route)),
|
|
237
|
+
).sort((left, right) => {
|
|
238
|
+
const leftCooling = isCoolingDown(left.provider)
|
|
239
|
+
const rightCooling = isCoolingDown(right.provider)
|
|
240
|
+
if (leftCooling !== rightCooling) return leftCooling ? 1 : -1
|
|
241
|
+
const leftRole = roleWeight(strategy, left.role)
|
|
242
|
+
const rightRole = roleWeight(strategy, right.role)
|
|
243
|
+
if (leftRole !== rightRole) return leftRole - rightRole
|
|
244
|
+
if (left.priority !== right.priority) return left.priority - right.priority
|
|
245
|
+
return left.label.localeCompare(right.label)
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function resolvePrimaryAgentRoute(
|
|
250
|
+
agent: Agent | null | undefined,
|
|
251
|
+
preferredStrategy?: AgentRoutingStrategy | null,
|
|
252
|
+
): ResolvedAgentRoute | null {
|
|
253
|
+
return resolveAgentRouteCandidates(agent, preferredStrategy)[0] || null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function applyResolvedRoute<T extends {
|
|
257
|
+
provider: ProviderType
|
|
258
|
+
model: string
|
|
259
|
+
credentialId?: string | null
|
|
260
|
+
fallbackCredentialIds?: string[]
|
|
261
|
+
apiEndpoint?: string | null
|
|
262
|
+
gatewayProfileId?: string | null
|
|
263
|
+
}>(
|
|
264
|
+
target: T,
|
|
265
|
+
route: ResolvedAgentRoute | null | undefined,
|
|
266
|
+
): T {
|
|
267
|
+
if (!route) return target
|
|
268
|
+
return {
|
|
269
|
+
...target,
|
|
270
|
+
provider: route.provider,
|
|
271
|
+
model: route.model,
|
|
272
|
+
credentialId: route.credentialId ?? null,
|
|
273
|
+
fallbackCredentialIds: [...route.fallbackCredentialIds],
|
|
274
|
+
apiEndpoint: route.apiEndpoint ?? null,
|
|
275
|
+
gatewayProfileId: route.gatewayProfileId ?? null,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -202,4 +202,63 @@ describe('approval auto-approve', () => {
|
|
|
202
202
|
assert.match(output.lastMessage.text, /\"type\":\"plugin_request\"/)
|
|
203
203
|
assert.match(output.lastMessage.text, /\"pluginId\":\"shell\"/)
|
|
204
204
|
})
|
|
205
|
+
|
|
206
|
+
it('applies tool access after a manual approval decision', () => {
|
|
207
|
+
const output = runWithTempDataDir(`
|
|
208
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
209
|
+
const approvalsMod = await import('./src/lib/server/approvals.ts')
|
|
210
|
+
const storage = storageMod.default || storageMod
|
|
211
|
+
const approvals = approvalsMod.default || approvalsMod
|
|
212
|
+
|
|
213
|
+
const now = Date.now()
|
|
214
|
+
storage.saveSettings({
|
|
215
|
+
approvalsEnabled: true,
|
|
216
|
+
approvalAutoApproveCategories: [],
|
|
217
|
+
})
|
|
218
|
+
storage.saveSessions({
|
|
219
|
+
session_manual: {
|
|
220
|
+
id: 'session_manual',
|
|
221
|
+
name: 'Manual Approval Test',
|
|
222
|
+
cwd: process.cwd(),
|
|
223
|
+
user: 'tester',
|
|
224
|
+
provider: 'openai',
|
|
225
|
+
model: 'gpt-test',
|
|
226
|
+
credentialId: null,
|
|
227
|
+
apiEndpoint: null,
|
|
228
|
+
claudeSessionId: null,
|
|
229
|
+
codexThreadId: null,
|
|
230
|
+
opencodeSessionId: null,
|
|
231
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
232
|
+
messages: [],
|
|
233
|
+
createdAt: now,
|
|
234
|
+
lastActiveAt: now,
|
|
235
|
+
sessionType: 'human',
|
|
236
|
+
agentId: 'default',
|
|
237
|
+
plugins: [],
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const approval = await approvals.requestApprovalMaybeAutoApprove({
|
|
242
|
+
category: 'tool_access',
|
|
243
|
+
title: 'Enable Plugin: shell',
|
|
244
|
+
description: 'Need shell access for a task.',
|
|
245
|
+
data: { toolId: 'shell', pluginId: 'shell' },
|
|
246
|
+
sessionId: 'session_manual',
|
|
247
|
+
agentId: 'default',
|
|
248
|
+
})
|
|
249
|
+
await approvals.submitDecision(approval.id, true)
|
|
250
|
+
|
|
251
|
+
const storedApproval = storage.loadApprovals()[approval.id]
|
|
252
|
+
const session = storage.loadSessions().session_manual
|
|
253
|
+
console.log(JSON.stringify({
|
|
254
|
+
initialStatus: approval.status,
|
|
255
|
+
finalStatus: storedApproval?.status || null,
|
|
256
|
+
plugins: session.plugins || [],
|
|
257
|
+
}))
|
|
258
|
+
`)
|
|
259
|
+
|
|
260
|
+
assert.equal(output.initialStatus, 'pending')
|
|
261
|
+
assert.equal(output.finalStatus, 'approved')
|
|
262
|
+
assert.equal(output.plugins.includes('shell'), true)
|
|
263
|
+
})
|
|
205
264
|
})
|