@bbigbang/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +380 -0
- package/dist/execution/executionDispatcher.js +3810 -0
- package/dist/main.js +90 -0
- package/dist/nodeEventHistory.js +206 -0
- package/dist/scheduler/dreamLogic.js +50 -0
- package/dist/scheduler/dreamScheduler.js +65 -0
- package/dist/services/agentFileAccessService.js +1913 -0
- package/dist/services/agentRuntimeCleanupBroker.js +62 -0
- package/dist/services/agentSkillsBroker.js +118 -0
- package/dist/services/agentSkillsService.js +83 -0
- package/dist/services/agentWorkspaceBroker.js +937 -0
- package/dist/services/agentWorkspaceService.js +70 -0
- package/dist/services/appVersion.js +14 -0
- package/dist/services/auth.js +586 -0
- package/dist/services/claudeControlBroker.js +154 -0
- package/dist/services/claudeTranscriptBroker.js +100 -0
- package/dist/services/claudeTranscriptService.js +359 -0
- package/dist/services/codexAppServerBroker.js +155 -0
- package/dist/services/codexTranscriptBroker.js +98 -0
- package/dist/services/codexTranscriptService.js +961 -0
- package/dist/services/droidMissionBroker.js +124 -0
- package/dist/services/droidMissionImporter.js +630 -0
- package/dist/services/droidModelOptions.js +165 -0
- package/dist/services/hubServerRegistrationService.js +268 -0
- package/dist/services/libraryManifest.js +43 -0
- package/dist/services/libraryScaffold.js +26 -0
- package/dist/services/libraryService.js +2263 -0
- package/dist/services/memoryService.js +386 -0
- package/dist/services/missionEvidence.js +377 -0
- package/dist/services/missionService.js +2361 -0
- package/dist/services/missionTrace.js +158 -0
- package/dist/services/nativeMissionBriefParser.js +120 -0
- package/dist/services/nativeMissionOrchestrator.js +2045 -0
- package/dist/services/nativeMissionReportGenerator.js +227 -0
- package/dist/services/nativeMissionValidationRunner.js +452 -0
- package/dist/services/nativeMissionWorkerBroker.js +190 -0
- package/dist/services/nodeRegistry.js +34 -0
- package/dist/services/nodeStateReconciler.js +97 -0
- package/dist/services/panelMediaScanner.js +119 -0
- package/dist/services/persistentRuntimeJsonlClient.js +153 -0
- package/dist/services/platformAgentPolicy.js +180 -0
- package/dist/services/platformAgentService.js +2041 -0
- package/dist/services/projectAccessResolver.js +93 -0
- package/dist/services/projectService.js +392 -0
- package/dist/services/resourceSpaceService.js +140 -0
- package/dist/services/scenarioRuntimeService.js +1130 -0
- package/dist/services/suggestedPlannerService.js +868 -0
- package/dist/services/workbenchGitBroker.js +161 -0
- package/dist/services/workbenchGitService.js +69 -0
- package/dist/services/workbenchInspectBroker.js +65 -0
- package/dist/services/workbenchNodePathService.js +79 -0
- package/dist/services/workbenchRegistryService.js +240 -0
- package/dist/services/workbenchRootService.js +181 -0
- package/dist/services/workbenchTerminalBroker.js +378 -0
- package/dist/services/workspaceRunOwnership.js +60 -0
- package/dist/services/workspaceScaffold.js +105 -0
- package/dist/services/workspaceSessionRuntimeService.js +576 -0
- package/dist/services/workspaceSessionService.js +245 -0
- package/dist/services/workspaceToolActionRunner.js +1582 -0
- package/dist/services/workspaceToolErrors.js +10 -0
- package/dist/services/workspaceToolExecutionUtils.js +895 -0
- package/dist/services/workspaceToolLatestStateProjector.js +91 -0
- package/dist/services/workspaceToolManifest.js +572 -0
- package/dist/services/workspaceToolMutationQueue.js +43 -0
- package/dist/services/workspaceToolPanelProjection.js +460 -0
- package/dist/services/workspaceToolPromotion.js +255 -0
- package/dist/services/workspaceToolPromotionState.js +224 -0
- package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
- package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
- package/dist/services/workspaceToolReadModel.js +378 -0
- package/dist/services/workspaceToolRunLedger.js +239 -0
- package/dist/services/workspaceToolService.js +3067 -0
- package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
- package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
- package/dist/services/workspaceToolTypes.js +1 -0
- package/dist/services/workspaceToolUploadMaterializer.js +228 -0
- package/dist/web/actionCardRoutes.js +129 -0
- package/dist/web/actionCards.js +469 -0
- package/dist/web/activationContext.js +684 -0
- package/dist/web/agentChannelGuards.js +48 -0
- package/dist/web/agentMentionCooldowns.js +32 -0
- package/dist/web/agentReminders.js +1668 -0
- package/dist/web/agentRuntimePresence.js +197 -0
- package/dist/web/agentSelfState.js +494 -0
- package/dist/web/agentTaskLinks.js +26 -0
- package/dist/web/agentVisibility.js +79 -0
- package/dist/web/assets.js +95 -0
- package/dist/web/channelActivationPrompt.js +395 -0
- package/dist/web/channelMemoryNotes.js +127 -0
- package/dist/web/channelMentions.js +10 -0
- package/dist/web/channelMessageSequences.js +19 -0
- package/dist/web/channelSubscriptions.js +26 -0
- package/dist/web/clearedTaskRoots.js +10 -0
- package/dist/web/collaborationPromptGuidance.js +36 -0
- package/dist/web/collaborationSurfaceState.js +140 -0
- package/dist/web/contextBundleRanking.js +154 -0
- package/dist/web/contextBundleResolver.js +488 -0
- package/dist/web/conversationBuiltinSkillRoots.js +50 -0
- package/dist/web/conversationControls.js +232 -0
- package/dist/web/conversationHandoffs.js +612 -0
- package/dist/web/conversationManager.js +2511 -0
- package/dist/web/conversationSummaries.js +876 -0
- package/dist/web/conversationSurfaceKinds.js +17 -0
- package/dist/web/conversationTargets.js +173 -0
- package/dist/web/directActivationPrompt.js +122 -0
- package/dist/web/directReplyTargets.js +69 -0
- package/dist/web/directThreadResolver.js +129 -0
- package/dist/web/dmTaskHandoffPrompt.js +120 -0
- package/dist/web/dmTaskThreadStatusProjection.js +229 -0
- package/dist/web/ftsQuery.js +33 -0
- package/dist/web/internalAgentRouter.js +11341 -0
- package/dist/web/libraryCuratorScheduler.js +58 -0
- package/dist/web/libraryDocumentPromptGuidance.js +8 -0
- package/dist/web/messageCheckpoints.js +19 -0
- package/dist/web/nodeWsHandler.js +2495 -0
- package/dist/web/notificationRounds.js +1061 -0
- package/dist/web/panelActionMessages.js +108 -0
- package/dist/web/panelActivationPrompt.js +18 -0
- package/dist/web/panelAudit.js +273 -0
- package/dist/web/panelLifecycle.js +222 -0
- package/dist/web/panelMediaPolicy.js +43 -0
- package/dist/web/panelPathPolicy.js +63 -0
- package/dist/web/panelPreviews.js +175 -0
- package/dist/web/panelQueryHandles.js +2749 -0
- package/dist/web/panelRoutes.js +2147 -0
- package/dist/web/panels.js +904 -0
- package/dist/web/peerInboxAggregates.js +1247 -0
- package/dist/web/planApprovalState.js +92 -0
- package/dist/web/platformAgentScheduler.js +66 -0
- package/dist/web/proactiveOpportunities.js +452 -0
- package/dist/web/promptContextSections.js +242 -0
- package/dist/web/promptHistorySanitizer.js +26 -0
- package/dist/web/promptSlashCommands.js +158 -0
- package/dist/web/rollingConversationSummary.js +453 -0
- package/dist/web/routeHelpers.js +11 -0
- package/dist/web/routes/handoff.js +288 -0
- package/dist/web/routes/history.js +345 -0
- package/dist/web/routes/memory.js +258 -0
- package/dist/web/routes/selfState.js +171 -0
- package/dist/web/routes/workspace.js +154 -0
- package/dist/web/runSurfaceWatermarks.js +431 -0
- package/dist/web/runtimeCapabilities.js +48 -0
- package/dist/web/sameAgentHandoffs.js +494 -0
- package/dist/web/server.js +15567 -0
- package/dist/web/sharedCollaborationCapsules.js +163 -0
- package/dist/web/soloSessionRelay.js +42 -0
- package/dist/web/soloWsHandler.js +138 -0
- package/dist/web/suggestedPlannerScheduler.js +56 -0
- package/dist/web/surfaceActivationPolicy.js +108 -0
- package/dist/web/surfaceCollaborators.js +61 -0
- package/dist/web/surfaceSystemStatus.js +263 -0
- package/dist/web/targetParticipants.js +77 -0
- package/dist/web/taskEvents.js +49 -0
- package/dist/web/taskLifecycleMessages.js +165 -0
- package/dist/web/taskLoops.js +732 -0
- package/dist/web/taskMemoryNotes.js +224 -0
- package/dist/web/taskNumbers.js +16 -0
- package/dist/web/taskOwnerGuards.js +49 -0
- package/dist/web/taskParticipantResolver.js +42 -0
- package/dist/web/taskParticipants.js +97 -0
- package/dist/web/taskSourceDetails.js +20 -0
- package/dist/web/taskStateViews.js +210 -0
- package/dist/web/taskStatusTransitions.js +9 -0
- package/dist/web/taskThreadFollowups.js +599 -0
- package/dist/web/taskThreadRuntimeClosure.js +685 -0
- package/dist/web/taskUpdateDelivery.js +104 -0
- package/dist/web/threadReplyContentHeuristics.js +30 -0
- package/dist/web/threadRoots.js +61 -0
- package/dist/web/threadTaskBindings.js +365 -0
- package/dist/web/uiPanelPromptGuidance.js +27 -0
- package/dist/web/workspaceMemoryHints.js +143 -0
- package/dist/web/workspaceToolPromptGuidance.js +30 -0
- package/dist/web/wsHandler.js +397 -0
- package/dist/web/wsSink.js +116 -0
- package/package.json +54 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { log } from '@bbigbang/runtime-acp';
|
|
2
|
+
const REALTIME_STATE_COALESCE_MS = 500;
|
|
3
|
+
export class WorkspaceToolLatestStateProjector {
|
|
4
|
+
deps;
|
|
5
|
+
pendingRealtimeStateUpdates = new Map();
|
|
6
|
+
constructor(deps) {
|
|
7
|
+
this.deps = deps;
|
|
8
|
+
}
|
|
9
|
+
hasPendingRealtimeStateUpdate(toolId) {
|
|
10
|
+
return this.pendingRealtimeStateUpdates.has(toolId);
|
|
11
|
+
}
|
|
12
|
+
// Persists latest state and rewrites the tool snapshot panel in one DB
|
|
13
|
+
// transaction. The state_updated broadcast is intentionally commit-after.
|
|
14
|
+
updateLatestState(params) {
|
|
15
|
+
this.deps.snapshotPanels.updateLatestState(params, {
|
|
16
|
+
onCommitted: () => this.deps.onStateUpdated?.(params),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
// Completion/status sync is diagnostic UI only; callers use the best-effort
|
|
20
|
+
// wrapper when a snapshot failure must not roll back run completion.
|
|
21
|
+
syncStatus(toolId, status) {
|
|
22
|
+
this.deps.snapshotPanels.syncForTool(toolId, status);
|
|
23
|
+
}
|
|
24
|
+
syncStatusBestEffort(toolId, status) {
|
|
25
|
+
try {
|
|
26
|
+
this.syncStatus(toolId, status);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
log.warn('[workspace-tools] failed to sync tool panel snapshot status', {
|
|
30
|
+
toolId,
|
|
31
|
+
status,
|
|
32
|
+
error: String(error?.message ?? error),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
scheduleRealtimeStateUpdate(ctx, state) {
|
|
37
|
+
const existing = this.pendingRealtimeStateUpdates.get(ctx.toolId);
|
|
38
|
+
if (existing) {
|
|
39
|
+
existing.state = state;
|
|
40
|
+
existing.userId = ctx.userId;
|
|
41
|
+
existing.agentId = ctx.agentId;
|
|
42
|
+
existing.maintenanceConversationId = ctx.maintenanceConversationId;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
this.flushPendingRealtimeStateForTool(ctx.toolId);
|
|
47
|
+
}, REALTIME_STATE_COALESCE_MS);
|
|
48
|
+
timer.unref?.();
|
|
49
|
+
this.pendingRealtimeStateUpdates.set(ctx.toolId, {
|
|
50
|
+
toolId: ctx.toolId,
|
|
51
|
+
userId: ctx.userId,
|
|
52
|
+
agentId: ctx.agentId,
|
|
53
|
+
maintenanceConversationId: ctx.maintenanceConversationId,
|
|
54
|
+
state,
|
|
55
|
+
timer,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
flushPendingRealtimeStateForTool(toolId) {
|
|
59
|
+
const pending = this.pendingRealtimeStateUpdates.get(toolId);
|
|
60
|
+
if (!pending)
|
|
61
|
+
return;
|
|
62
|
+
this.pendingRealtimeStateUpdates.delete(toolId);
|
|
63
|
+
clearTimeout(pending.timer);
|
|
64
|
+
try {
|
|
65
|
+
this.updateLatestState({
|
|
66
|
+
toolId: pending.toolId,
|
|
67
|
+
userId: pending.userId,
|
|
68
|
+
agentId: pending.agentId,
|
|
69
|
+
maintenanceConversationId: pending.maintenanceConversationId,
|
|
70
|
+
state: pending.state,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
log.warn('[workspace-tools] failed to flush realtime tool state', {
|
|
75
|
+
toolId,
|
|
76
|
+
error: String(error?.message ?? error),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
clearPendingRealtimeStateForTool(toolId) {
|
|
81
|
+
const pending = this.pendingRealtimeStateUpdates.get(toolId);
|
|
82
|
+
if (!pending)
|
|
83
|
+
return;
|
|
84
|
+
clearTimeout(pending.timer);
|
|
85
|
+
this.pendingRealtimeStateUpdates.delete(toolId);
|
|
86
|
+
}
|
|
87
|
+
getToolInfoForActionViewerAfterPendingRealtimeFlush(params) {
|
|
88
|
+
this.flushPendingRealtimeStateForTool(params.toolId);
|
|
89
|
+
return this.deps.getToolInfoForActionViewer(params.viewerUserId, params.toolId, params.allowAdminBypass);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { WORKSPACE_TOOL_PANEL_ACTION_PARAM_NAMESPACE, buildWorkspaceToolPanelActionParamKey, collectPanelLevelUnsupportedNodes, validatePanelTemplateProps, } from '@bbigbang/protocol';
|
|
3
|
+
import { WorkspaceToolServiceError } from './workspaceToolErrors.js';
|
|
4
|
+
const WORKSPACE_TOOL_ENV_VAR_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5
|
+
const WORKSPACE_TOOL_CONDA_ENV_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
6
|
+
const WORKSPACE_TOOL_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
7
|
+
const WORKSPACE_TOOL_PARAM_INPUTS = ['text', 'textarea', 'number', 'checkbox', 'select', 'multiselect', 'tags', 'date', 'file'];
|
|
8
|
+
const WORKSPACE_TOOL_FILE_PARAM_MAX_BYTES = 5 * 1024 * 1024;
|
|
9
|
+
const workspaceToolParamBoundSchema = z.union([z.number().finite(), z.string()]);
|
|
10
|
+
const workspaceToolParamFieldSchema = z.object({
|
|
11
|
+
name: z.string().trim().min(1),
|
|
12
|
+
label: z.string().trim().min(1).optional(),
|
|
13
|
+
description: z.string().trim().min(1).optional(),
|
|
14
|
+
input: z.enum(WORKSPACE_TOOL_PARAM_INPUTS).optional(),
|
|
15
|
+
required: z.boolean().optional(),
|
|
16
|
+
placeholder: z.string().optional(),
|
|
17
|
+
defaultValue: z.unknown().optional(),
|
|
18
|
+
min: workspaceToolParamBoundSchema.optional(),
|
|
19
|
+
max: workspaceToolParamBoundSchema.optional(),
|
|
20
|
+
accept: z.string().trim().min(1).optional(),
|
|
21
|
+
multiple: z.boolean().optional(),
|
|
22
|
+
maxBytes: z.number().int().positive().max(WORKSPACE_TOOL_FILE_PARAM_MAX_BYTES).optional(),
|
|
23
|
+
options: z.array(z.object({
|
|
24
|
+
label: z.string().trim().min(1),
|
|
25
|
+
value: z.string(),
|
|
26
|
+
})).optional(),
|
|
27
|
+
});
|
|
28
|
+
const workspaceToolActionManifestSchema = z.object({
|
|
29
|
+
actionId: z.string().trim().min(1),
|
|
30
|
+
label: z.string().trim().min(1),
|
|
31
|
+
description: z.string().trim().min(1).optional(),
|
|
32
|
+
kind: z.enum(['start', 'stop', 'status', 'restart', 'custom']),
|
|
33
|
+
mode: z.enum(['platform_exec', 'notify_agent']).optional(),
|
|
34
|
+
command: z.string().optional(),
|
|
35
|
+
cwd: z.string().optional(),
|
|
36
|
+
persistent: z.boolean().optional(),
|
|
37
|
+
maxRunSeconds: z.number().int().positive().optional(),
|
|
38
|
+
idleTimeoutSeconds: z.number().int().positive().optional(),
|
|
39
|
+
paramsSchema: z.array(workspaceToolParamFieldSchema).optional(),
|
|
40
|
+
});
|
|
41
|
+
const workspaceToolExecutionEnvSchema = z.object({
|
|
42
|
+
condaEnv: z.string().trim().min(1).optional(),
|
|
43
|
+
vars: z.record(z.string(), z.string()).optional(),
|
|
44
|
+
cwd: z.string().trim().min(1).optional(),
|
|
45
|
+
}).optional();
|
|
46
|
+
const workspaceToolCliManifestSchema = z.object({
|
|
47
|
+
entry: z.string().trim().min(1),
|
|
48
|
+
}).optional();
|
|
49
|
+
const workspaceToolRuntimeManifestSchema = z.object({
|
|
50
|
+
mode: z.enum(['per_action', 'persistent_runtime']).optional(),
|
|
51
|
+
idleTtlSeconds: z.number().int().positive().optional(),
|
|
52
|
+
entry: z.string().trim().min(1).optional(),
|
|
53
|
+
logDir: z.string().trim().min(1).optional(),
|
|
54
|
+
}).optional();
|
|
55
|
+
const workspaceToolManifestSchema = z.object({
|
|
56
|
+
slug: z.string().trim().min(1),
|
|
57
|
+
name: z.string().trim().min(1),
|
|
58
|
+
description: z.string().default(''),
|
|
59
|
+
icon: z.string().trim().min(1).optional(),
|
|
60
|
+
defaultCwd: z.string().trim().min(1).optional(),
|
|
61
|
+
env: workspaceToolExecutionEnvSchema,
|
|
62
|
+
cli: workspaceToolCliManifestSchema,
|
|
63
|
+
runtime: workspaceToolRuntimeManifestSchema,
|
|
64
|
+
scope: z.enum(['private', 'channel', 'public']).optional(),
|
|
65
|
+
scopeChannelId: z.string().trim().min(1).optional().nullable(),
|
|
66
|
+
allowSharedExec: z.boolean().optional(),
|
|
67
|
+
service: z.object({
|
|
68
|
+
port: z.number().int().min(1).max(65535),
|
|
69
|
+
healthPath: z.string().trim().min(1).optional(),
|
|
70
|
+
}).optional(),
|
|
71
|
+
actions: z.array(workspaceToolActionManifestSchema),
|
|
72
|
+
view: z.object({
|
|
73
|
+
title: z.string().trim().min(1).optional(),
|
|
74
|
+
fields: z.array(z.object({
|
|
75
|
+
name: z.string().trim().min(1),
|
|
76
|
+
label: z.string().trim().min(1).optional(),
|
|
77
|
+
type: z.enum(['string', 'number', 'boolean']),
|
|
78
|
+
filterable: z.boolean().optional(),
|
|
79
|
+
sortable: z.boolean().optional(),
|
|
80
|
+
})),
|
|
81
|
+
mediaSlots: z.array(z.object({
|
|
82
|
+
name: z.string().trim().min(1),
|
|
83
|
+
kind: z.literal('workspace_path'),
|
|
84
|
+
displayType: z.enum(['img', 'latex', 'text']),
|
|
85
|
+
})).optional(),
|
|
86
|
+
summary: z.unknown().optional(),
|
|
87
|
+
parameterForm: z.unknown().optional(),
|
|
88
|
+
template: z.unknown(),
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
const TOOL_PARAMETER_FORM_TEXT_TYPES = new Set(['TextInput', 'TextArea']);
|
|
92
|
+
const TOOL_PARAMETER_FORM_NUMBER_TYPES = new Set(['NumberInput', 'Slider']);
|
|
93
|
+
const TOOL_PARAMETER_FORM_SELECT_TYPES = new Set(['Select']);
|
|
94
|
+
const TOOL_PARAMETER_FORM_MULTISELECT_TYPES = new Set(['MultiSelect']);
|
|
95
|
+
const TOOL_PARAMETER_FORM_TAG_TYPES = new Set(['TagInput']);
|
|
96
|
+
const TOOL_PARAMETER_FORM_DATE_TYPES = new Set(['DatePicker']);
|
|
97
|
+
const TOOL_PARAMETER_FORM_FILE_TYPES = new Set(['FileUpload']);
|
|
98
|
+
const TOOL_PARAMETER_FORM_UNSUPPORTED_TYPES = new Set(['Button', 'RunStatusCard', 'RunLogViewer', 'ArtifactList']);
|
|
99
|
+
const TOOL_SUMMARY_UNSUPPORTED_TYPES = new Set([
|
|
100
|
+
'ActionBar',
|
|
101
|
+
'TextInput',
|
|
102
|
+
'TextArea',
|
|
103
|
+
'NumberInput',
|
|
104
|
+
'Checkbox',
|
|
105
|
+
'Select',
|
|
106
|
+
'MultiSelect',
|
|
107
|
+
'TagInput',
|
|
108
|
+
'Slider',
|
|
109
|
+
'DatePicker',
|
|
110
|
+
'FileUpload',
|
|
111
|
+
'Button',
|
|
112
|
+
]);
|
|
113
|
+
export function parseWorkspaceToolManifest(raw) {
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(raw);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new WorkspaceToolServiceError('Tool manifest is not valid JSON.', 400);
|
|
120
|
+
}
|
|
121
|
+
const result = workspaceToolManifestSchema.safeParse(parsed);
|
|
122
|
+
if (!result.success) {
|
|
123
|
+
throw new WorkspaceToolServiceError(`Tool manifest is invalid: ${result.error.issues[0]?.message ?? 'unknown error'}`, 400);
|
|
124
|
+
}
|
|
125
|
+
const manifest = result.data;
|
|
126
|
+
if (manifest.scope === 'channel' && !(manifest.scopeChannelId?.trim())) {
|
|
127
|
+
throw new WorkspaceToolServiceError('Channel-scoped tools must declare scopeChannelId.', 400);
|
|
128
|
+
}
|
|
129
|
+
validateWorkspaceToolExecutionEnv(manifest);
|
|
130
|
+
validateWorkspaceToolRuntime(manifest);
|
|
131
|
+
validateWorkspaceToolActionParams(manifest);
|
|
132
|
+
const actionIds = new Set();
|
|
133
|
+
for (const action of manifest.actions) {
|
|
134
|
+
if (actionIds.has(action.actionId)) {
|
|
135
|
+
throw new WorkspaceToolServiceError(`Duplicate tool action id "${action.actionId}".`, 400);
|
|
136
|
+
}
|
|
137
|
+
actionIds.add(action.actionId);
|
|
138
|
+
if (manifest.runtime?.mode === 'persistent_runtime' && action.kind === 'restart') {
|
|
139
|
+
throw new WorkspaceToolServiceError('Persistent runtime tools do not support restart actions; use a custom action or stop and invoke again.', 400);
|
|
140
|
+
}
|
|
141
|
+
const hasExecutionEntry = manifest.cli?.entry?.trim() || (manifest.runtime?.mode === 'persistent_runtime' && manifest.runtime.entry?.trim());
|
|
142
|
+
if (actionRequiresCommand(action) && !action.command?.trim() && !hasExecutionEntry) {
|
|
143
|
+
throw new WorkspaceToolServiceError(`Tool action "${action.actionId}" must declare a command or use manifest cli.entry.`, 400);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
validateWorkspaceToolView(manifest);
|
|
147
|
+
return manifest;
|
|
148
|
+
}
|
|
149
|
+
export function withPersistedToolSettings(manifest, settings) {
|
|
150
|
+
return {
|
|
151
|
+
...manifest,
|
|
152
|
+
scope: settings.scope,
|
|
153
|
+
scopeChannelId: settings.scope === 'channel' ? settings.scopeChannelId ?? null : null,
|
|
154
|
+
allowSharedExec: settings.allowSharedExec,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export function sanitizeWorkspaceToolManifestForViewer(manifest, settings) {
|
|
158
|
+
const persisted = withPersistedToolSettings(manifest, settings);
|
|
159
|
+
if (settings.isOwner) {
|
|
160
|
+
return persisted;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
...persisted,
|
|
164
|
+
defaultCwd: undefined,
|
|
165
|
+
env: undefined,
|
|
166
|
+
cli: undefined,
|
|
167
|
+
runtime: undefined,
|
|
168
|
+
actions: persisted.actions.map((action) => ({
|
|
169
|
+
actionId: action.actionId,
|
|
170
|
+
label: action.label,
|
|
171
|
+
description: action.description,
|
|
172
|
+
kind: action.kind,
|
|
173
|
+
mode: action.mode,
|
|
174
|
+
persistent: action.persistent,
|
|
175
|
+
maxRunSeconds: action.maxRunSeconds,
|
|
176
|
+
paramsSchema: action.paramsSchema,
|
|
177
|
+
cwd: undefined,
|
|
178
|
+
command: undefined,
|
|
179
|
+
})),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function resolveWorkspaceToolActionMode(action) {
|
|
183
|
+
return action.mode ?? 'platform_exec';
|
|
184
|
+
}
|
|
185
|
+
export function actionRequiresCommand(action) {
|
|
186
|
+
return resolveWorkspaceToolActionMode(action) === 'platform_exec'
|
|
187
|
+
&& (action.kind === 'start'
|
|
188
|
+
|| action.kind === 'status'
|
|
189
|
+
|| action.kind === 'restart'
|
|
190
|
+
|| action.kind === 'custom'
|
|
191
|
+
|| action.persistent === true);
|
|
192
|
+
}
|
|
193
|
+
export function resolveWorkspaceToolActionParamBinding(actions, key) {
|
|
194
|
+
if (typeof key !== 'string')
|
|
195
|
+
return null;
|
|
196
|
+
const namespacePrefix = `${WORKSPACE_TOOL_PANEL_ACTION_PARAM_NAMESPACE}.`;
|
|
197
|
+
if (!key.startsWith(namespacePrefix))
|
|
198
|
+
return null;
|
|
199
|
+
const remainder = key.slice(namespacePrefix.length);
|
|
200
|
+
const actionIds = [...new Set(actions.map((action) => action.actionId))]
|
|
201
|
+
.filter((actionId) => actionId.length > 0)
|
|
202
|
+
.sort((left, right) => right.length - left.length);
|
|
203
|
+
for (const actionId of actionIds) {
|
|
204
|
+
const actionPrefix = `${actionId}.`;
|
|
205
|
+
if (remainder.startsWith(actionPrefix) && remainder.length > actionPrefix.length) {
|
|
206
|
+
return {
|
|
207
|
+
actionId,
|
|
208
|
+
paramName: remainder.slice(actionPrefix.length),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
function validateWorkspaceToolExecutionEnv(manifest) {
|
|
215
|
+
const env = manifest.env;
|
|
216
|
+
if (!env)
|
|
217
|
+
return;
|
|
218
|
+
if (env.condaEnv && !WORKSPACE_TOOL_CONDA_ENV_PATTERN.test(env.condaEnv)) {
|
|
219
|
+
throw new WorkspaceToolServiceError('Tool execution env.condaEnv may only contain letters, numbers, dot, underscore, or dash.', 400);
|
|
220
|
+
}
|
|
221
|
+
for (const key of Object.keys(env.vars ?? {})) {
|
|
222
|
+
if (!WORKSPACE_TOOL_ENV_VAR_NAME_PATTERN.test(key)) {
|
|
223
|
+
throw new WorkspaceToolServiceError(`Tool execution env var "${key}" is invalid.`, 400);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function validateWorkspaceToolRuntime(manifest) {
|
|
228
|
+
const runtime = manifest.runtime;
|
|
229
|
+
if (!runtime)
|
|
230
|
+
return;
|
|
231
|
+
const mode = runtime.mode ?? 'per_action';
|
|
232
|
+
if (mode === 'persistent_runtime' && !(runtime.entry?.trim() || manifest.cli?.entry?.trim())) {
|
|
233
|
+
throw new WorkspaceToolServiceError('Persistent tool runtime must declare runtime.entry or cli.entry.', 400);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function validateWorkspaceToolActionParams(manifest) {
|
|
237
|
+
for (const action of manifest.actions) {
|
|
238
|
+
const paramNames = new Set();
|
|
239
|
+
for (const field of action.paramsSchema ?? []) {
|
|
240
|
+
if (paramNames.has(field.name)) {
|
|
241
|
+
throw new WorkspaceToolServiceError(`Tool action "${action.actionId}" declares duplicate parameter "${field.name}".`, 400);
|
|
242
|
+
}
|
|
243
|
+
paramNames.add(field.name);
|
|
244
|
+
if ((field.input ?? 'text') === 'file' && resolveWorkspaceToolActionMode(action) !== 'platform_exec') {
|
|
245
|
+
throw new WorkspaceToolServiceError(`Tool action "${action.actionId}" file parameter "${field.name}" requires platform_exec mode.`, 400);
|
|
246
|
+
}
|
|
247
|
+
validateWorkspaceToolParamField(action.actionId, field);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function validateWorkspaceToolParamField(actionId, field) {
|
|
252
|
+
const input = field.input ?? 'text';
|
|
253
|
+
if ((input === 'select' || input === 'multiselect') && !field.options?.length) {
|
|
254
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" must declare static options for ${input}.`, 400);
|
|
255
|
+
}
|
|
256
|
+
if (field.options?.length && input !== 'select' && input !== 'multiselect') {
|
|
257
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" options are only supported for select or multiselect inputs.`, 400);
|
|
258
|
+
}
|
|
259
|
+
if (input !== 'file' && (field.accept !== undefined || field.multiple !== undefined || field.maxBytes !== undefined)) {
|
|
260
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" upload options are only supported for file inputs.`, 400);
|
|
261
|
+
}
|
|
262
|
+
if (input === 'file' && field.defaultValue !== undefined) {
|
|
263
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" file inputs do not support defaultValue.`, 400);
|
|
264
|
+
}
|
|
265
|
+
const optionValues = new Set();
|
|
266
|
+
for (const option of field.options ?? []) {
|
|
267
|
+
if (optionValues.has(option.value)) {
|
|
268
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" has duplicate option value "${option.value}".`, 400);
|
|
269
|
+
}
|
|
270
|
+
optionValues.add(option.value);
|
|
271
|
+
}
|
|
272
|
+
validateWorkspaceToolParamBounds(actionId, field, input);
|
|
273
|
+
validateWorkspaceToolParamDefault(actionId, field, input, optionValues);
|
|
274
|
+
}
|
|
275
|
+
function validateWorkspaceToolParamBounds(actionId, field, input) {
|
|
276
|
+
const bounds = [
|
|
277
|
+
['min', field.min],
|
|
278
|
+
['max', field.max],
|
|
279
|
+
];
|
|
280
|
+
for (const [key, value] of bounds) {
|
|
281
|
+
if (value === undefined)
|
|
282
|
+
continue;
|
|
283
|
+
if (input === 'number') {
|
|
284
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
285
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" ${key} must be a finite number for number inputs.`, 400);
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (input === 'date') {
|
|
290
|
+
if (typeof value !== 'string' || !isValidWorkspaceToolDate(value)) {
|
|
291
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" ${key} must be a YYYY-MM-DD date for date inputs.`, 400);
|
|
292
|
+
}
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (input === 'file') {
|
|
296
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" ${key} is not supported for file inputs; use maxBytes instead.`, 400);
|
|
297
|
+
}
|
|
298
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" ${key} is only supported for number or date inputs.`, 400);
|
|
299
|
+
}
|
|
300
|
+
if (input === 'number' && typeof field.min === 'number' && typeof field.max === 'number' && field.min > field.max) {
|
|
301
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" min cannot exceed max.`, 400);
|
|
302
|
+
}
|
|
303
|
+
if (input === 'date' && typeof field.min === 'string' && typeof field.max === 'string' && field.min > field.max) {
|
|
304
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" min cannot exceed max.`, 400);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function validateWorkspaceToolParamDefault(actionId, field, input, optionValues) {
|
|
308
|
+
if (field.defaultValue === undefined || field.defaultValue === null || field.defaultValue === '')
|
|
309
|
+
return;
|
|
310
|
+
const defaultValue = field.defaultValue;
|
|
311
|
+
if (input === 'number') {
|
|
312
|
+
const numeric = typeof defaultValue === 'number' ? defaultValue : Number(defaultValue);
|
|
313
|
+
if (!Number.isFinite(numeric)) {
|
|
314
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must be a number.`, 400);
|
|
315
|
+
}
|
|
316
|
+
if (typeof field.min === 'number' && numeric < field.min) {
|
|
317
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must be at least ${field.min}.`, 400);
|
|
318
|
+
}
|
|
319
|
+
if (typeof field.max === 'number' && numeric > field.max) {
|
|
320
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must be at most ${field.max}.`, 400);
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (input === 'checkbox') {
|
|
325
|
+
if (typeof defaultValue !== 'boolean') {
|
|
326
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must be a boolean.`, 400);
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (input === 'select') {
|
|
331
|
+
if (typeof defaultValue !== 'string' || !optionValues.has(defaultValue)) {
|
|
332
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must match a declared option.`, 400);
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (input === 'multiselect') {
|
|
337
|
+
if (!Array.isArray(defaultValue) || !defaultValue.every((item) => typeof item === 'string' && optionValues.has(item))) {
|
|
338
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must be an array of declared option values.`, 400);
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (input === 'tags') {
|
|
343
|
+
if (!Array.isArray(defaultValue) && typeof defaultValue !== 'string') {
|
|
344
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must be a string or string array.`, 400);
|
|
345
|
+
}
|
|
346
|
+
if (Array.isArray(defaultValue) && !defaultValue.every((item) => typeof item === 'string')) {
|
|
347
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must be a string array.`, 400);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (input === 'date') {
|
|
352
|
+
if (typeof defaultValue !== 'string' || !isValidWorkspaceToolDate(defaultValue)) {
|
|
353
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must be a YYYY-MM-DD date.`, 400);
|
|
354
|
+
}
|
|
355
|
+
if (typeof field.min === 'string' && defaultValue < field.min) {
|
|
356
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must be on or after ${field.min}.`, 400);
|
|
357
|
+
}
|
|
358
|
+
if (typeof field.max === 'string' && defaultValue > field.max) {
|
|
359
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must be on or before ${field.max}.`, 400);
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (input === 'textarea') {
|
|
364
|
+
if (typeof defaultValue !== 'string') {
|
|
365
|
+
throw new WorkspaceToolServiceError(`Tool action "${actionId}" parameter "${field.name}" defaultValue must be a string.`, 400);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function isValidWorkspaceToolDate(value) {
|
|
370
|
+
if (!WORKSPACE_TOOL_DATE_PATTERN.test(value))
|
|
371
|
+
return false;
|
|
372
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
373
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day))
|
|
374
|
+
return false;
|
|
375
|
+
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
376
|
+
return parsed.getUTCFullYear() === year
|
|
377
|
+
&& parsed.getUTCMonth() === month - 1
|
|
378
|
+
&& parsed.getUTCDate() === day;
|
|
379
|
+
}
|
|
380
|
+
function validateWorkspaceToolView(manifest) {
|
|
381
|
+
for (const slot of manifest.view.mediaSlots ?? []) {
|
|
382
|
+
if (slot.kind !== 'workspace_path') {
|
|
383
|
+
throw new WorkspaceToolServiceError(`Tool media slot "${slot.name}" must use kind "workspace_path".`, 400);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const validation = validatePanelTemplateProps('RowTemplateGrid', {
|
|
387
|
+
title: manifest.view.title,
|
|
388
|
+
fields: manifest.view.fields,
|
|
389
|
+
mediaSlots: manifest.view.mediaSlots ?? [],
|
|
390
|
+
summary: manifest.view.summary,
|
|
391
|
+
parameterForm: manifest.view.parameterForm,
|
|
392
|
+
template: manifest.view.template,
|
|
393
|
+
}, manifest.actions.map((action) => ({ id: action.actionId })));
|
|
394
|
+
if (!validation.ok) {
|
|
395
|
+
throw new WorkspaceToolServiceError(`Tool manifest is invalid: ${validation.errors[0] ?? 'invalid view template'}`, 400);
|
|
396
|
+
}
|
|
397
|
+
validateWorkspaceToolSummaryNode(manifest.view.summary);
|
|
398
|
+
validateWorkspaceToolParameterFormBindings(manifest);
|
|
399
|
+
}
|
|
400
|
+
function validateWorkspaceToolSummaryNode(summary) {
|
|
401
|
+
if (!summary)
|
|
402
|
+
return;
|
|
403
|
+
const unsupported = collectPanelLevelUnsupportedNodes(summary, {
|
|
404
|
+
unsupportedTypes: TOOL_SUMMARY_UNSUPPORTED_TYPES,
|
|
405
|
+
});
|
|
406
|
+
if (unsupported.length > 0) {
|
|
407
|
+
throw new WorkspaceToolServiceError('Tool view.summary is display-only and does not support action or parameter controls; put action parameters in view.parameterForm.', 400);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function validateWorkspaceToolParameterFormBindings(manifest) {
|
|
411
|
+
const parameterForm = manifest.view.parameterForm;
|
|
412
|
+
if (!parameterForm)
|
|
413
|
+
return;
|
|
414
|
+
const actionsById = new Map(manifest.actions.map((action) => [action.actionId, action]));
|
|
415
|
+
const boundParams = new Set();
|
|
416
|
+
collectWorkspaceToolParameterFormBindings(parameterForm, manifest.actions, actionsById, boundParams);
|
|
417
|
+
for (const action of manifest.actions) {
|
|
418
|
+
for (const field of action.paramsSchema ?? []) {
|
|
419
|
+
if (field.required === true && field.defaultValue === undefined) {
|
|
420
|
+
const key = buildWorkspaceToolPanelActionParamKey(action.actionId, field.name);
|
|
421
|
+
if (!boundParams.has(key)) {
|
|
422
|
+
throw new WorkspaceToolServiceError(`Tool parameter form must expose required parameter "${action.actionId}.${field.name}".`, 400);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function collectWorkspaceToolParameterFormBindings(node, actions, actionsById, boundParams) {
|
|
429
|
+
switch (node.type) {
|
|
430
|
+
case 'Section':
|
|
431
|
+
case 'ParameterFormSection':
|
|
432
|
+
case 'Card':
|
|
433
|
+
case 'Stack':
|
|
434
|
+
case 'Inline':
|
|
435
|
+
case 'Grid':
|
|
436
|
+
for (const child of node.children) {
|
|
437
|
+
collectWorkspaceToolParameterFormBindings(child, actions, actionsById, boundParams);
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
case 'Columns':
|
|
441
|
+
for (const column of node.columns) {
|
|
442
|
+
for (const child of column.children) {
|
|
443
|
+
collectWorkspaceToolParameterFormBindings(child, actions, actionsById, boundParams);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
case 'ActionBar':
|
|
448
|
+
return;
|
|
449
|
+
case 'RunStatusCard':
|
|
450
|
+
case 'RunLogViewer':
|
|
451
|
+
case 'ArtifactList':
|
|
452
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control "${node.type}" is not supported. Use TextInput, TextArea, NumberInput, Slider, Checkbox, Select, MultiSelect, TagInput, DatePicker, or FileUpload with declared tool action params.`, 400);
|
|
453
|
+
case 'TextInput':
|
|
454
|
+
case 'TextArea':
|
|
455
|
+
case 'NumberInput':
|
|
456
|
+
case 'Checkbox':
|
|
457
|
+
case 'Select':
|
|
458
|
+
case 'MultiSelect':
|
|
459
|
+
case 'TagInput':
|
|
460
|
+
case 'Slider':
|
|
461
|
+
case 'DatePicker':
|
|
462
|
+
case 'FileUpload':
|
|
463
|
+
case 'Button': {
|
|
464
|
+
if (TOOL_PARAMETER_FORM_UNSUPPORTED_TYPES.has(node.type)) {
|
|
465
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control "${node.type}" is not supported. Use TextInput, TextArea, NumberInput, Slider, Checkbox, Select, MultiSelect, TagInput, DatePicker, or FileUpload with declared tool action params.`, 400);
|
|
466
|
+
}
|
|
467
|
+
const rawName = isPlainRecord(node.props) ? node.props.name : undefined;
|
|
468
|
+
if (typeof rawName !== 'string' || !rawName.trim()) {
|
|
469
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control "${node.type}" must declare props.name.`, 400);
|
|
470
|
+
}
|
|
471
|
+
const binding = resolveWorkspaceToolActionParamBinding(actions, rawName);
|
|
472
|
+
if (!binding) {
|
|
473
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control "${rawName}" must use the tool_action.<actionId>.<paramName> namespace.`, 400);
|
|
474
|
+
}
|
|
475
|
+
const action = actionsById.get(binding.actionId);
|
|
476
|
+
if (!action) {
|
|
477
|
+
throw new WorkspaceToolServiceError(`Tool parameter form references unknown action "${binding.actionId}".`, 400);
|
|
478
|
+
}
|
|
479
|
+
const field = (action.paramsSchema ?? []).find((candidate) => candidate.name === binding.paramName);
|
|
480
|
+
if (!field) {
|
|
481
|
+
throw new WorkspaceToolServiceError(`Tool parameter form references unknown parameter "${binding.actionId}.${binding.paramName}".`, 400);
|
|
482
|
+
}
|
|
483
|
+
validateWorkspaceToolParameterControlType(node.type, field, binding);
|
|
484
|
+
validateWorkspaceToolParameterControlProps(node.type, node.props, field, binding);
|
|
485
|
+
boundParams.add(buildWorkspaceToolPanelActionParamKey(binding.actionId, binding.paramName));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
default:
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function validateWorkspaceToolParameterControlProps(nodeType, props, field, binding) {
|
|
493
|
+
if (props.defaultValue !== undefined && !valuesDeepEqual(props.defaultValue, field.defaultValue)) {
|
|
494
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" defaultValue must match paramsSchema.defaultValue.`, 400);
|
|
495
|
+
}
|
|
496
|
+
if (props.value !== undefined && !valuesDeepEqual(props.value, field.defaultValue)) {
|
|
497
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" value must match paramsSchema.defaultValue.`, 400);
|
|
498
|
+
}
|
|
499
|
+
if (props.required !== undefined && props.required !== (field.required === true)) {
|
|
500
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" required must match paramsSchema.required.`, 400);
|
|
501
|
+
}
|
|
502
|
+
if (props.min !== undefined && !valuesDeepEqual(props.min, field.min)) {
|
|
503
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" min must match paramsSchema.min.`, 400);
|
|
504
|
+
}
|
|
505
|
+
if (props.max !== undefined && !valuesDeepEqual(props.max, field.max)) {
|
|
506
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" max must match paramsSchema.max.`, 400);
|
|
507
|
+
}
|
|
508
|
+
if ((nodeType === 'Select' || nodeType === 'MultiSelect')
|
|
509
|
+
&& props.options !== undefined
|
|
510
|
+
&& !valuesDeepEqual(props.options, field.options)) {
|
|
511
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" options must match paramsSchema.options.`, 400);
|
|
512
|
+
}
|
|
513
|
+
if (nodeType === 'FileUpload') {
|
|
514
|
+
if (props.accept !== undefined && !valuesDeepEqual(props.accept, field.accept)) {
|
|
515
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" accept must match paramsSchema.accept.`, 400);
|
|
516
|
+
}
|
|
517
|
+
if (props.multiple !== undefined && !valuesDeepEqual(props.multiple, field.multiple === true)) {
|
|
518
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" multiple must match paramsSchema.multiple.`, 400);
|
|
519
|
+
}
|
|
520
|
+
if (props.maxBytes !== undefined && !valuesDeepEqual(props.maxBytes, field.maxBytes)) {
|
|
521
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" maxBytes must match paramsSchema.maxBytes.`, 400);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function validateWorkspaceToolParameterControlType(nodeType, field, binding) {
|
|
526
|
+
const fieldInput = field.input ?? 'text';
|
|
527
|
+
if (fieldInput === 'checkbox' && nodeType !== 'Checkbox') {
|
|
528
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" must use Checkbox.`, 400);
|
|
529
|
+
}
|
|
530
|
+
if (fieldInput === 'number' && !TOOL_PARAMETER_FORM_NUMBER_TYPES.has(nodeType)) {
|
|
531
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" must use NumberInput or Slider.`, 400);
|
|
532
|
+
}
|
|
533
|
+
if (fieldInput === 'select' && !TOOL_PARAMETER_FORM_SELECT_TYPES.has(nodeType)) {
|
|
534
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" must use Select.`, 400);
|
|
535
|
+
}
|
|
536
|
+
if (fieldInput === 'multiselect' && !TOOL_PARAMETER_FORM_MULTISELECT_TYPES.has(nodeType)) {
|
|
537
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" must use MultiSelect.`, 400);
|
|
538
|
+
}
|
|
539
|
+
if (fieldInput === 'tags' && !TOOL_PARAMETER_FORM_TAG_TYPES.has(nodeType)) {
|
|
540
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" must use TagInput.`, 400);
|
|
541
|
+
}
|
|
542
|
+
if (fieldInput === 'date' && !TOOL_PARAMETER_FORM_DATE_TYPES.has(nodeType)) {
|
|
543
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" must use DatePicker.`, 400);
|
|
544
|
+
}
|
|
545
|
+
if (fieldInput === 'textarea' && nodeType !== 'TextArea') {
|
|
546
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" must use TextArea.`, 400);
|
|
547
|
+
}
|
|
548
|
+
if (fieldInput === 'file' && !TOOL_PARAMETER_FORM_FILE_TYPES.has(nodeType)) {
|
|
549
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" must use FileUpload.`, 400);
|
|
550
|
+
}
|
|
551
|
+
if (fieldInput === 'text' && !TOOL_PARAMETER_FORM_TEXT_TYPES.has(nodeType)) {
|
|
552
|
+
throw new WorkspaceToolServiceError(`Tool parameter form control for "${binding.actionId}.${binding.paramName}" must use TextInput or TextArea.`, 400);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function valuesDeepEqual(left, right) {
|
|
556
|
+
return stableStringify(left) === stableStringify(right);
|
|
557
|
+
}
|
|
558
|
+
function stableStringify(value) {
|
|
559
|
+
if (value === undefined)
|
|
560
|
+
return 'undefined';
|
|
561
|
+
if (value === null || typeof value !== 'object')
|
|
562
|
+
return JSON.stringify(value) ?? 'undefined';
|
|
563
|
+
if (Array.isArray(value))
|
|
564
|
+
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
565
|
+
return `{${Object.entries(value)
|
|
566
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
567
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)
|
|
568
|
+
.join(',')}}`;
|
|
569
|
+
}
|
|
570
|
+
function isPlainRecord(value) {
|
|
571
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
572
|
+
}
|