@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,895 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { WorkspaceToolServiceError } from './workspaceToolErrors.js';
|
|
3
|
+
const ONE_SHOT_POLL_INTERVAL_MS = 250;
|
|
4
|
+
const ONE_SHOT_TIMEOUT_MS = 120_000;
|
|
5
|
+
const WORKSPACE_TOOL_RUN_EVENT_PAYLOAD_MAX_CHARS = 16_000;
|
|
6
|
+
const WORKSPACE_TOOL_RUN_ARTIFACT_MAX_COUNT = 50;
|
|
7
|
+
const WORKSPACE_TOOL_RUN_ARTIFACT_PATH_MAX_CHARS = 1_000;
|
|
8
|
+
const WORKSPACE_TOOL_RUN_ARTIFACTS_MAX_CHARS = 16_000;
|
|
9
|
+
const WORKSPACE_TOOL_RUN_EVENT_BASE64_MIN_CHARS = 2048;
|
|
10
|
+
const WORKSPACE_TOOL_RUN_EVENT_BASE64_PLACEHOLDER = '[omitted base64 data]';
|
|
11
|
+
const WORKSPACE_TOOL_RUN_EVENT_BASE64_FRAGMENT_PATTERN = new RegExp(`[A-Za-z0-9+/]{${WORKSPACE_TOOL_RUN_EVENT_BASE64_MIN_CHARS},}={0,2}`, 'g');
|
|
12
|
+
const WORKSPACE_TOOL_RUN_EVENT_WRAPPED_BASE64_FRAGMENT_PATTERN = /(^|[^A-Za-z0-9+/])((?:[A-Za-z0-9+/]{64,}(?:(?:\\r|\\n|\\t|[\r\n\t ])+)){1,}[A-Za-z0-9+/]{64,}={0,2})(?=$|[^A-Za-z0-9+/=])/g;
|
|
13
|
+
export const CLI_STOP_CLEANUP_TIMEOUT_MS = 5_000;
|
|
14
|
+
const WORKSPACE_TOOL_RUN_EVENT_TYPES = new Set([
|
|
15
|
+
'log',
|
|
16
|
+
'progress',
|
|
17
|
+
'result',
|
|
18
|
+
'error',
|
|
19
|
+
'artifact',
|
|
20
|
+
]);
|
|
21
|
+
export function getPublishedWorkspaceToolBundleRoot(toolId, revision) {
|
|
22
|
+
return path.posix.join('.workspace-tool-published', toolId, `r${revision}`);
|
|
23
|
+
}
|
|
24
|
+
function normalizeWorkspaceToolMediaRelativePath(rawPath) {
|
|
25
|
+
const trimmed = rawPath.trim();
|
|
26
|
+
if (/^data:/i.test(trimmed) || /;base64,/i.test(trimmed))
|
|
27
|
+
return null;
|
|
28
|
+
const compact = trimmed.replace(/\s+/g, '');
|
|
29
|
+
if (compact.length >= WORKSPACE_TOOL_RUN_EVENT_BASE64_MIN_CHARS
|
|
30
|
+
&& compact.length % 4 === 0
|
|
31
|
+
&& /^[A-Za-z0-9+/]+={0,2}$/.test(compact)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const normalized = path.posix.normalize(trimmed.replace(/\\/g, '/'));
|
|
35
|
+
if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../') || path.posix.isAbsolute(normalized)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
export function getWorkspaceToolMediaRelativePathCandidates(tool, rawPath, options) {
|
|
41
|
+
const normalized = normalizeWorkspaceToolMediaRelativePath(rawPath);
|
|
42
|
+
if (!normalized)
|
|
43
|
+
return [];
|
|
44
|
+
const sourceBundleRoot = normalizeWorkspaceToolMediaRelativePath(tool.sourceBundleRoot);
|
|
45
|
+
const publishedBundleRoot = getPublishedWorkspaceToolBundleRoot(tool.toolId, tool.revision);
|
|
46
|
+
const candidates = [];
|
|
47
|
+
const push = (candidate) => {
|
|
48
|
+
const normalizedValue = normalizeWorkspaceToolMediaRelativePath(candidate.relativePath);
|
|
49
|
+
if (!normalizedValue)
|
|
50
|
+
return;
|
|
51
|
+
if (candidates.some((existing) => existing.relativePath === normalizedValue))
|
|
52
|
+
return;
|
|
53
|
+
candidates.push({ ...candidate, relativePath: normalizedValue });
|
|
54
|
+
};
|
|
55
|
+
if (normalized === publishedBundleRoot || normalized.startsWith(`${publishedBundleRoot}/`)) {
|
|
56
|
+
push({
|
|
57
|
+
relativePath: normalized,
|
|
58
|
+
allowedRelativeRoot: publishedBundleRoot,
|
|
59
|
+
source: 'published',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else if (sourceBundleRoot && (normalized === sourceBundleRoot || normalized.startsWith(`${sourceBundleRoot}/`))) {
|
|
63
|
+
const suffix = normalized.slice(sourceBundleRoot.length).replace(/^\/+/, '');
|
|
64
|
+
push({
|
|
65
|
+
relativePath: suffix ? path.posix.join(publishedBundleRoot, suffix) : publishedBundleRoot,
|
|
66
|
+
allowedRelativeRoot: publishedBundleRoot,
|
|
67
|
+
source: 'published',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
push({
|
|
72
|
+
relativePath: path.posix.join(publishedBundleRoot, normalized),
|
|
73
|
+
allowedRelativeRoot: publishedBundleRoot,
|
|
74
|
+
source: 'published',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (options?.includeWorkspaceFallback) {
|
|
78
|
+
push({ relativePath: normalized, source: 'workspace_fallback' });
|
|
79
|
+
}
|
|
80
|
+
return candidates;
|
|
81
|
+
}
|
|
82
|
+
export function runRowToInfo(row, actionKind) {
|
|
83
|
+
return {
|
|
84
|
+
runId: row.runId,
|
|
85
|
+
toolId: row.toolId,
|
|
86
|
+
toolRevision: row.toolRevision ?? null,
|
|
87
|
+
actionId: row.actionId,
|
|
88
|
+
actionKind,
|
|
89
|
+
terminalId: row.terminalId ?? null,
|
|
90
|
+
status: row.status,
|
|
91
|
+
params: parseJsonRecord(row.paramsJson),
|
|
92
|
+
exitCode: row.exitCode ?? null,
|
|
93
|
+
signal: row.signal ?? null,
|
|
94
|
+
startedAt: row.startedAt,
|
|
95
|
+
endedAt: row.endedAt ?? null,
|
|
96
|
+
requestedByUserId: row.requestedByUserId ?? null,
|
|
97
|
+
requestedByUsername: row.requestedByUsername ?? null,
|
|
98
|
+
requestedByAgentId: row.requestedByAgentId ?? null,
|
|
99
|
+
requestedByAgentName: row.requestedByAgentName ?? null,
|
|
100
|
+
executionTargetKind: row.executionTargetKind ?? null,
|
|
101
|
+
executionAgentId: row.executionAgentId ?? null,
|
|
102
|
+
executionNodeId: row.executionNodeId ?? null,
|
|
103
|
+
executionHostname: row.executionHostname ?? null,
|
|
104
|
+
executionWorkspacePath: row.executionWorkspacePath ?? null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function runEventRowToInfo(row) {
|
|
108
|
+
return {
|
|
109
|
+
eventId: row.eventId,
|
|
110
|
+
runId: row.runId,
|
|
111
|
+
toolId: row.toolId,
|
|
112
|
+
sequence: row.sequence,
|
|
113
|
+
type: row.eventType,
|
|
114
|
+
level: row.level ?? null,
|
|
115
|
+
summary: row.summary ?? null,
|
|
116
|
+
payload: parseJsonRecord(row.payloadJson),
|
|
117
|
+
artifacts: parseWorkspaceToolRunArtifactRefs(row.artifactsJson),
|
|
118
|
+
createdAt: row.createdAt,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function validateManifestConsistency(workspaceRoot, manifestPath, manifest) {
|
|
122
|
+
const expected = `.agent-tools/${manifest.slug}/tool.json`;
|
|
123
|
+
if (manifestPath !== expected) {
|
|
124
|
+
throw new WorkspaceToolServiceError(`Manifest path must be ${expected}.`, 400);
|
|
125
|
+
}
|
|
126
|
+
normalizeWorkspaceRelativePath(workspaceRoot, manifestPath);
|
|
127
|
+
}
|
|
128
|
+
export function validateManifestCwds(workspaceRoot, manifest, sourceBundleRoot) {
|
|
129
|
+
if (manifest.defaultCwd) {
|
|
130
|
+
resolveToolPath(workspaceRoot, manifest.defaultCwd);
|
|
131
|
+
}
|
|
132
|
+
if (manifest.env?.cwd) {
|
|
133
|
+
resolveToolPath(workspaceRoot, manifest.env.cwd);
|
|
134
|
+
}
|
|
135
|
+
if (manifest.cli?.entry) {
|
|
136
|
+
resolveToolBundlePath(workspaceRoot, sourceBundleRoot, manifest.cli.entry, 'Tool CLI entry');
|
|
137
|
+
}
|
|
138
|
+
if (manifest.runtime?.entry) {
|
|
139
|
+
resolveToolBundlePath(workspaceRoot, sourceBundleRoot, manifest.runtime.entry, 'Tool runtime entry');
|
|
140
|
+
}
|
|
141
|
+
if (manifest.runtime?.logDir) {
|
|
142
|
+
normalizeWorkspaceRelativePath(workspaceRoot, manifest.runtime.logDir);
|
|
143
|
+
}
|
|
144
|
+
for (const action of manifest.actions) {
|
|
145
|
+
if (action.cwd) {
|
|
146
|
+
resolveToolPath(workspaceRoot, action.cwd);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function resolveToolBundlePath(workspaceRoot, bundleRoot, candidate, label) {
|
|
151
|
+
const trimmed = candidate.trim();
|
|
152
|
+
if (!trimmed || path.isAbsolute(trimmed)) {
|
|
153
|
+
throw new WorkspaceToolServiceError(`${label} must stay inside the tool bundle.`, 400);
|
|
154
|
+
}
|
|
155
|
+
const bundleAbsolutePath = resolveToolPath(workspaceRoot, bundleRoot);
|
|
156
|
+
const absolutePath = path.resolve(bundleAbsolutePath, trimmed);
|
|
157
|
+
const relativePath = path.relative(bundleAbsolutePath, absolutePath);
|
|
158
|
+
if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
|
|
159
|
+
throw new WorkspaceToolServiceError(`${label} must stay inside the tool bundle.`, 400);
|
|
160
|
+
}
|
|
161
|
+
return absolutePath;
|
|
162
|
+
}
|
|
163
|
+
export function normalizeWorkspaceRelativePath(workspaceRoot, requestedPath) {
|
|
164
|
+
const normalized = requestedPath.trim();
|
|
165
|
+
if (!normalized) {
|
|
166
|
+
throw new WorkspaceToolServiceError('manifest_path is required.', 400);
|
|
167
|
+
}
|
|
168
|
+
const absolutePath = resolveToolPath(workspaceRoot, normalized);
|
|
169
|
+
const relativePath = path.relative(workspaceRoot, absolutePath).replace(/\\/g, '/');
|
|
170
|
+
if (!relativePath || relativePath.startsWith('../') || relativePath === '..') {
|
|
171
|
+
throw new WorkspaceToolServiceError('Tool path must remain inside the agent workspace.', 400);
|
|
172
|
+
}
|
|
173
|
+
return { absolutePath, relativePath };
|
|
174
|
+
}
|
|
175
|
+
export function resolveToolPath(workspaceRoot, candidate) {
|
|
176
|
+
const absolutePath = path.isAbsolute(candidate)
|
|
177
|
+
? path.normalize(candidate)
|
|
178
|
+
: path.resolve(workspaceRoot, candidate);
|
|
179
|
+
const relativePath = path.relative(workspaceRoot, absolutePath);
|
|
180
|
+
if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
|
|
181
|
+
throw new WorkspaceToolServiceError('Tool path must remain inside the agent workspace.', 400);
|
|
182
|
+
}
|
|
183
|
+
return absolutePath;
|
|
184
|
+
}
|
|
185
|
+
export function resolveToolCwd(workspaceRoot, manifest, action, sourceBundleRoot, publishedBundleRoot) {
|
|
186
|
+
const requested = action.cwd?.trim() || manifest.env?.cwd?.trim() || manifest.defaultCwd?.trim() || publishedBundleRoot;
|
|
187
|
+
return rewriteWorkspaceToolPathToPublished(workspaceRoot, requested, sourceBundleRoot, publishedBundleRoot);
|
|
188
|
+
}
|
|
189
|
+
export function buildWorkspaceToolStartupCommand(params) {
|
|
190
|
+
const command = params.action.command?.trim()
|
|
191
|
+
? substituteCommand(params.action.command, params.actionParams)
|
|
192
|
+
: buildWorkspaceToolCliCommand(params.manifest, params.action, params.actionParams, {
|
|
193
|
+
workspaceRoot: params.workspaceRoot,
|
|
194
|
+
publishedBundleRoot: params.publishedBundleRoot,
|
|
195
|
+
});
|
|
196
|
+
return applyWorkspaceToolExecutionEnv(command, params.manifest.env);
|
|
197
|
+
}
|
|
198
|
+
export function buildWorkspaceToolRuntimeCommand(params) {
|
|
199
|
+
const entry = params.manifest.runtime?.entry?.trim() || params.manifest.cli?.entry?.trim();
|
|
200
|
+
if (!entry)
|
|
201
|
+
return '';
|
|
202
|
+
const executable = resolveToolBundlePath(params.workspaceRoot, params.publishedBundleRoot, entry, 'Tool runtime entry');
|
|
203
|
+
const command = [
|
|
204
|
+
shellEscape(executable),
|
|
205
|
+
'runtime',
|
|
206
|
+
'--tool-id',
|
|
207
|
+
shellEscape(params.toolId),
|
|
208
|
+
'--revision',
|
|
209
|
+
shellEscape(params.revision),
|
|
210
|
+
'--log-dir',
|
|
211
|
+
shellEscape(resolveToolPath(params.workspaceRoot, params.logDir)),
|
|
212
|
+
].join(' ');
|
|
213
|
+
return applyWorkspaceToolExecutionEnv(command, params.manifest.env);
|
|
214
|
+
}
|
|
215
|
+
function buildWorkspaceToolCliCommand(manifest, action, actionParams, paths) {
|
|
216
|
+
const entry = manifest.cli?.entry?.trim();
|
|
217
|
+
if (!entry)
|
|
218
|
+
return '';
|
|
219
|
+
const subcommand = workspaceToolCliSubcommandForAction(action);
|
|
220
|
+
const executable = resolveToolBundlePath(paths.workspaceRoot, paths.publishedBundleRoot, entry, 'Tool CLI entry');
|
|
221
|
+
return [
|
|
222
|
+
shellEscape(executable),
|
|
223
|
+
...subcommand.map((part) => shellEscape(part)),
|
|
224
|
+
'--action-id',
|
|
225
|
+
shellEscape(action.actionId),
|
|
226
|
+
'--params-json',
|
|
227
|
+
shellEscape(JSON.stringify(actionParams)),
|
|
228
|
+
].join(' ');
|
|
229
|
+
}
|
|
230
|
+
function workspaceToolCliSubcommandForAction(action) {
|
|
231
|
+
if (action.kind === 'start')
|
|
232
|
+
return ['start'];
|
|
233
|
+
if (action.kind === 'restart')
|
|
234
|
+
return ['start'];
|
|
235
|
+
if (action.kind === 'status')
|
|
236
|
+
return ['status'];
|
|
237
|
+
if (action.kind === 'stop')
|
|
238
|
+
return ['stop'];
|
|
239
|
+
return ['action', action.actionId];
|
|
240
|
+
}
|
|
241
|
+
function applyWorkspaceToolExecutionEnv(command, env) {
|
|
242
|
+
const trimmedCommand = command.trim();
|
|
243
|
+
if (!trimmedCommand || !env)
|
|
244
|
+
return trimmedCommand;
|
|
245
|
+
const envAssignments = Object.entries(env.vars ?? {})
|
|
246
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
247
|
+
.map(([key, value]) => `${key}=${shellEscape(value)}`);
|
|
248
|
+
const prefix = envAssignments.join(' ');
|
|
249
|
+
if (env.condaEnv) {
|
|
250
|
+
return [
|
|
251
|
+
prefix,
|
|
252
|
+
`conda run -n ${shellEscape(env.condaEnv)} --no-capture-output sh -lc ${shellEscape(trimmedCommand)}`,
|
|
253
|
+
].filter(Boolean).join(' ');
|
|
254
|
+
}
|
|
255
|
+
if (prefix) {
|
|
256
|
+
return `${prefix} sh -lc ${shellEscape(trimmedCommand)}`;
|
|
257
|
+
}
|
|
258
|
+
return trimmedCommand;
|
|
259
|
+
}
|
|
260
|
+
export function rewriteWorkspaceToolCommandPaths(workspaceRoot, command, sourceBundleRoot, publishedBundleRoot) {
|
|
261
|
+
if (!command.trim())
|
|
262
|
+
return command;
|
|
263
|
+
if (!sourceBundleRoot.trim())
|
|
264
|
+
return command;
|
|
265
|
+
const replacements = [
|
|
266
|
+
[sourceBundleRoot, publishedBundleRoot],
|
|
267
|
+
[resolveToolPath(workspaceRoot, sourceBundleRoot), resolveToolPath(workspaceRoot, publishedBundleRoot)],
|
|
268
|
+
];
|
|
269
|
+
return replacements.reduce((current, [from, to]) => replaceAll(current, from, to), command);
|
|
270
|
+
}
|
|
271
|
+
function rewriteWorkspaceToolPathToPublished(workspaceRoot, candidate, sourceBundleRoot, publishedBundleRoot) {
|
|
272
|
+
if (!sourceBundleRoot.trim()) {
|
|
273
|
+
return resolveToolPath(workspaceRoot, candidate);
|
|
274
|
+
}
|
|
275
|
+
const publishedBundleAbsolutePath = resolveToolPath(workspaceRoot, publishedBundleRoot);
|
|
276
|
+
const sourceBundleAbsolutePath = resolveToolPath(workspaceRoot, sourceBundleRoot);
|
|
277
|
+
const candidateAbsolutePath = resolveToolPath(workspaceRoot, candidate);
|
|
278
|
+
const candidateRelativeToSource = path.relative(sourceBundleAbsolutePath, candidateAbsolutePath);
|
|
279
|
+
if (candidateRelativeToSource
|
|
280
|
+
&& candidateRelativeToSource !== '..'
|
|
281
|
+
&& !candidateRelativeToSource.startsWith(`..${path.sep}`)) {
|
|
282
|
+
return path.join(publishedBundleAbsolutePath, candidateRelativeToSource);
|
|
283
|
+
}
|
|
284
|
+
if (candidateAbsolutePath === sourceBundleAbsolutePath) {
|
|
285
|
+
return publishedBundleAbsolutePath;
|
|
286
|
+
}
|
|
287
|
+
return candidateAbsolutePath;
|
|
288
|
+
}
|
|
289
|
+
function replaceAll(input, from, to) {
|
|
290
|
+
return input.split(from).join(to);
|
|
291
|
+
}
|
|
292
|
+
export function formatBoundedJsonPreview(value, limit) {
|
|
293
|
+
const text = JSON.stringify(value, null, 2);
|
|
294
|
+
return truncateText(text, limit);
|
|
295
|
+
}
|
|
296
|
+
export function formatBoundedTextPreview(value, limit) {
|
|
297
|
+
const text = value?.trim();
|
|
298
|
+
if (!text)
|
|
299
|
+
return null;
|
|
300
|
+
return truncateText(scrubWorkspaceToolInlineBinaryText(text), limit);
|
|
301
|
+
}
|
|
302
|
+
function truncateText(value, limit) {
|
|
303
|
+
if (value.length <= limit)
|
|
304
|
+
return value;
|
|
305
|
+
return `${value.slice(0, Math.max(0, limit - 3))}...`;
|
|
306
|
+
}
|
|
307
|
+
export function normalizeActionParams(action, inputParams) {
|
|
308
|
+
const fields = action.paramsSchema ?? [];
|
|
309
|
+
const next = {};
|
|
310
|
+
for (const field of fields) {
|
|
311
|
+
const rawValue = Object.prototype.hasOwnProperty.call(inputParams, field.name)
|
|
312
|
+
? inputParams[field.name]
|
|
313
|
+
: field.defaultValue;
|
|
314
|
+
const input = field.input ?? 'text';
|
|
315
|
+
if (field.required && isMissingWorkspaceToolParamValue(rawValue, input)) {
|
|
316
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" is required.`, 400);
|
|
317
|
+
}
|
|
318
|
+
if (rawValue === undefined)
|
|
319
|
+
continue;
|
|
320
|
+
if (input === 'number' && rawValue !== null && rawValue !== '') {
|
|
321
|
+
const numeric = typeof rawValue === 'number' ? rawValue : Number(rawValue);
|
|
322
|
+
if (!Number.isFinite(numeric)) {
|
|
323
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" must be a number.`, 400);
|
|
324
|
+
}
|
|
325
|
+
if (typeof field.min === 'number' && numeric < field.min) {
|
|
326
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" must be at least ${field.min}.`, 400);
|
|
327
|
+
}
|
|
328
|
+
if (typeof field.max === 'number' && numeric > field.max) {
|
|
329
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" must be at most ${field.max}.`, 400);
|
|
330
|
+
}
|
|
331
|
+
next[field.name] = numeric;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (input === 'checkbox') {
|
|
335
|
+
next[field.name] = rawValue === true || rawValue === 'true' || rawValue === 1;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (input === 'select') {
|
|
339
|
+
if (rawValue === null || rawValue === '')
|
|
340
|
+
continue;
|
|
341
|
+
const textValue = String(rawValue);
|
|
342
|
+
if (field.options?.length && !field.options.some((option) => option.value === textValue)) {
|
|
343
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" has an invalid value.`, 400);
|
|
344
|
+
}
|
|
345
|
+
next[field.name] = textValue;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (input === 'multiselect') {
|
|
349
|
+
if (rawValue === null || rawValue === '')
|
|
350
|
+
continue;
|
|
351
|
+
if (!Array.isArray(rawValue)) {
|
|
352
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" must be an array.`, 400);
|
|
353
|
+
}
|
|
354
|
+
const values = normalizeStringArrayParam(field.name, rawValue);
|
|
355
|
+
const validOptions = new Set((field.options ?? []).map((option) => option.value));
|
|
356
|
+
if (validOptions.size > 0 && values.some((value) => !validOptions.has(value))) {
|
|
357
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" has an invalid value.`, 400);
|
|
358
|
+
}
|
|
359
|
+
next[field.name] = values;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (input === 'tags') {
|
|
363
|
+
if (rawValue === null || rawValue === '')
|
|
364
|
+
continue;
|
|
365
|
+
next[field.name] = normalizeTagParam(field.name, rawValue);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (input === 'date') {
|
|
369
|
+
if (rawValue === null || rawValue === '')
|
|
370
|
+
continue;
|
|
371
|
+
if (typeof rawValue !== 'string' || !isValidWorkspaceToolDateParam(rawValue)) {
|
|
372
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" must be a YYYY-MM-DD date.`, 400);
|
|
373
|
+
}
|
|
374
|
+
if (typeof field.min === 'string' && rawValue < field.min) {
|
|
375
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" must be on or after ${field.min}.`, 400);
|
|
376
|
+
}
|
|
377
|
+
if (typeof field.max === 'string' && rawValue > field.max) {
|
|
378
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" must be on or before ${field.max}.`, 400);
|
|
379
|
+
}
|
|
380
|
+
next[field.name] = rawValue;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (input === 'file') {
|
|
384
|
+
if (rawValue === null || rawValue === '')
|
|
385
|
+
continue;
|
|
386
|
+
if (field.multiple === true) {
|
|
387
|
+
if (!Array.isArray(rawValue)) {
|
|
388
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" must be an array of uploaded file refs.`, 400);
|
|
389
|
+
}
|
|
390
|
+
next[field.name] = rawValue.map((item) => normalizeUploadedFileRefParam(field.name, item, field.maxBytes));
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
if (Array.isArray(rawValue)) {
|
|
394
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" must be a single uploaded file ref.`, 400);
|
|
395
|
+
}
|
|
396
|
+
next[field.name] = normalizeUploadedFileRefParam(field.name, rawValue, field.maxBytes);
|
|
397
|
+
}
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (input === 'textarea') {
|
|
401
|
+
if (rawValue === null || rawValue === '')
|
|
402
|
+
continue;
|
|
403
|
+
if (typeof rawValue !== 'string') {
|
|
404
|
+
throw new WorkspaceToolServiceError(`Parameter "${field.name}" must be a string.`, 400);
|
|
405
|
+
}
|
|
406
|
+
next[field.name] = rawValue;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
next[field.name] = rawValue;
|
|
410
|
+
}
|
|
411
|
+
return next;
|
|
412
|
+
}
|
|
413
|
+
function isMissingWorkspaceToolParamValue(value, input) {
|
|
414
|
+
if (value === undefined || value === null || value === '')
|
|
415
|
+
return true;
|
|
416
|
+
if ((input === 'multiselect' || input === 'tags') && Array.isArray(value) && value.length === 0)
|
|
417
|
+
return true;
|
|
418
|
+
if (input === 'file' && Array.isArray(value) && value.length === 0)
|
|
419
|
+
return true;
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
function normalizeUploadedFileRefParam(fieldName, value, maxBytes) {
|
|
423
|
+
if (!isPlainRecord(value)) {
|
|
424
|
+
throw new WorkspaceToolServiceError(`Parameter "${fieldName}" must be an uploaded file ref.`, 400);
|
|
425
|
+
}
|
|
426
|
+
const assetId = normalizeUploadedAssetId(value.assetId)
|
|
427
|
+
?? normalizeUploadedAssetId(value.attachmentId)
|
|
428
|
+
?? normalizeUploadedAssetId(extractAttachmentIdFromUri(value.uri));
|
|
429
|
+
if (!assetId) {
|
|
430
|
+
throw new WorkspaceToolServiceError(`Parameter "${fieldName}" must reference an uploaded asset.`, 400);
|
|
431
|
+
}
|
|
432
|
+
const filename = typeof value.filename === 'string' && value.filename.trim()
|
|
433
|
+
? value.filename.trim()
|
|
434
|
+
: 'upload.bin';
|
|
435
|
+
const sizeBytes = typeof value.sizeBytes === 'number' && Number.isFinite(value.sizeBytes) && value.sizeBytes >= 0
|
|
436
|
+
? Math.floor(value.sizeBytes)
|
|
437
|
+
: undefined;
|
|
438
|
+
if (maxBytes !== undefined && sizeBytes !== undefined && sizeBytes > maxBytes) {
|
|
439
|
+
throw new WorkspaceToolServiceError(`Parameter "${fieldName}" file exceeds ${maxBytes} bytes.`, 400);
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
assetId,
|
|
443
|
+
attachmentId: assetId,
|
|
444
|
+
uri: `attachment:${assetId}`,
|
|
445
|
+
filename,
|
|
446
|
+
...(sizeBytes !== undefined ? { sizeBytes } : {}),
|
|
447
|
+
...(typeof value.kind === 'string' ? { kind: value.kind } : {}),
|
|
448
|
+
...(typeof value.scopeType === 'string' ? { scopeType: value.scopeType } : {}),
|
|
449
|
+
...(typeof value.scopeId === 'string' ? { scopeId: value.scopeId } : {}),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function normalizeUploadedAssetId(value) {
|
|
453
|
+
if (typeof value !== 'string')
|
|
454
|
+
return null;
|
|
455
|
+
const normalized = value.trim();
|
|
456
|
+
return /^[a-f0-9-]{36}$/i.test(normalized) ? normalized.toLowerCase() : null;
|
|
457
|
+
}
|
|
458
|
+
function extractAttachmentIdFromUri(value) {
|
|
459
|
+
if (typeof value !== 'string')
|
|
460
|
+
return null;
|
|
461
|
+
const normalized = value.trim();
|
|
462
|
+
if (!normalized.startsWith('attachment:'))
|
|
463
|
+
return null;
|
|
464
|
+
return normalized.slice('attachment:'.length).trim();
|
|
465
|
+
}
|
|
466
|
+
function normalizeStringArrayParam(fieldName, value) {
|
|
467
|
+
const result = [];
|
|
468
|
+
const seen = new Set();
|
|
469
|
+
for (const item of value) {
|
|
470
|
+
if (typeof item !== 'string') {
|
|
471
|
+
throw new WorkspaceToolServiceError(`Parameter "${fieldName}" must contain only strings.`, 400);
|
|
472
|
+
}
|
|
473
|
+
if (seen.has(item))
|
|
474
|
+
continue;
|
|
475
|
+
seen.add(item);
|
|
476
|
+
result.push(item);
|
|
477
|
+
}
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
function normalizeTagParam(fieldName, value) {
|
|
481
|
+
const rawItems = Array.isArray(value)
|
|
482
|
+
? value
|
|
483
|
+
: typeof value === 'string'
|
|
484
|
+
? value.split(/[,\n]/u)
|
|
485
|
+
: [];
|
|
486
|
+
if (!Array.isArray(value) && typeof value !== 'string') {
|
|
487
|
+
throw new WorkspaceToolServiceError(`Parameter "${fieldName}" must be a string or string array.`, 400);
|
|
488
|
+
}
|
|
489
|
+
const result = [];
|
|
490
|
+
const seen = new Set();
|
|
491
|
+
for (const item of rawItems) {
|
|
492
|
+
if (typeof item !== 'string') {
|
|
493
|
+
throw new WorkspaceToolServiceError(`Parameter "${fieldName}" must contain only strings.`, 400);
|
|
494
|
+
}
|
|
495
|
+
const tag = item.trim();
|
|
496
|
+
if (!tag || seen.has(tag))
|
|
497
|
+
continue;
|
|
498
|
+
seen.add(tag);
|
|
499
|
+
result.push(tag);
|
|
500
|
+
}
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
function isValidWorkspaceToolDateParam(value) {
|
|
504
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
505
|
+
return false;
|
|
506
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
507
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day))
|
|
508
|
+
return false;
|
|
509
|
+
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
510
|
+
return parsed.getUTCFullYear() === year
|
|
511
|
+
&& parsed.getUTCMonth() === month - 1
|
|
512
|
+
&& parsed.getUTCDate() === day;
|
|
513
|
+
}
|
|
514
|
+
function substituteCommand(command, params) {
|
|
515
|
+
return command.replace(/\{\{\s*([a-zA-Z0-9_-]+)\s*\}\}/g, (_match, name) => shellEscape(params[name]));
|
|
516
|
+
}
|
|
517
|
+
export function shellEscape(value) {
|
|
518
|
+
if (value === undefined || value === null)
|
|
519
|
+
return "''";
|
|
520
|
+
const text = typeof value === 'string'
|
|
521
|
+
? value
|
|
522
|
+
: typeof value === 'number' || typeof value === 'boolean'
|
|
523
|
+
? String(value)
|
|
524
|
+
: JSON.stringify(value);
|
|
525
|
+
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
526
|
+
}
|
|
527
|
+
export function parseStateRowJson(raw) {
|
|
528
|
+
if (!raw)
|
|
529
|
+
return null;
|
|
530
|
+
try {
|
|
531
|
+
const parsed = JSON.parse(raw);
|
|
532
|
+
return normalizeStateRow(parsed);
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
export function extractWorkspaceToolBusinessStatus(state) {
|
|
539
|
+
const value = state?.fields?.status;
|
|
540
|
+
return typeof value === 'string' && value.trim()
|
|
541
|
+
? value.trim().toLowerCase()
|
|
542
|
+
: null;
|
|
543
|
+
}
|
|
544
|
+
export function parseStateRowFromOutput(raw, strict) {
|
|
545
|
+
const trimmed = raw.trim();
|
|
546
|
+
if (!trimmed) {
|
|
547
|
+
if (strict) {
|
|
548
|
+
throw new WorkspaceToolServiceError('Status action returned empty output.', 400);
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
const parsed = JSON.parse(trimmed);
|
|
554
|
+
if (isJsonlEventEnvelope(parsed)) {
|
|
555
|
+
return parseStateRowFromJsonlEvent(parsed);
|
|
556
|
+
}
|
|
557
|
+
return normalizeStateRow(parsed, { rejectUnsupportedMediaKinds: true });
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
if (error instanceof WorkspaceToolServiceError) {
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
563
|
+
const eventState = parseStateRowFromJsonlOutput(trimmed);
|
|
564
|
+
if (eventState)
|
|
565
|
+
return eventState;
|
|
566
|
+
if (strict) {
|
|
567
|
+
throw new WorkspaceToolServiceError('Status action output must be a single JSON object.', 400);
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
export function parseStateRowFromJsonlOutput(raw) {
|
|
573
|
+
let latestState = null;
|
|
574
|
+
for (const line of raw.split('\n')) {
|
|
575
|
+
const trimmed = line.trim();
|
|
576
|
+
if (!trimmed)
|
|
577
|
+
continue;
|
|
578
|
+
let parsed;
|
|
579
|
+
try {
|
|
580
|
+
parsed = JSON.parse(trimmed);
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const candidate = parseStateRowFromJsonlEvent(parsed);
|
|
586
|
+
if (candidate)
|
|
587
|
+
latestState = candidate;
|
|
588
|
+
}
|
|
589
|
+
return latestState;
|
|
590
|
+
}
|
|
591
|
+
export function parseStateRowFromJsonlEvent(value) {
|
|
592
|
+
if (!isPlainRecord(value))
|
|
593
|
+
return null;
|
|
594
|
+
const eventType = typeof value.type === 'string' ? value.type.trim() : '';
|
|
595
|
+
if (eventType !== 'state')
|
|
596
|
+
return null;
|
|
597
|
+
if (isPlainRecord(value.state)) {
|
|
598
|
+
return normalizeStateRow(value.state, { rejectUnsupportedMediaKinds: true });
|
|
599
|
+
}
|
|
600
|
+
return normalizeStateRow(value, { rejectUnsupportedMediaKinds: true });
|
|
601
|
+
}
|
|
602
|
+
export function isJsonlEventEnvelope(value) {
|
|
603
|
+
return isPlainRecord(value) && typeof value.type === 'string';
|
|
604
|
+
}
|
|
605
|
+
export function normalizeWorkspaceToolRunEventEnvelope(value) {
|
|
606
|
+
if (!isJsonlEventEnvelope(value))
|
|
607
|
+
return null;
|
|
608
|
+
const eventType = value.type.trim();
|
|
609
|
+
if (!WORKSPACE_TOOL_RUN_EVENT_TYPES.has(eventType))
|
|
610
|
+
return null;
|
|
611
|
+
const artifacts = normalizeWorkspaceToolRunArtifacts(value);
|
|
612
|
+
const artifactsJson = serializeBoundedRunEventArtifacts(artifacts);
|
|
613
|
+
const summary = typeof value.summary === 'string' && value.summary.trim()
|
|
614
|
+
? scrubWorkspaceToolInlineBinaryText(value.summary.trim()).slice(0, 1000)
|
|
615
|
+
: null;
|
|
616
|
+
return {
|
|
617
|
+
eventType,
|
|
618
|
+
level: typeof value.level === 'string' && value.level.trim() ? value.level.trim().slice(0, 64) : null,
|
|
619
|
+
summary,
|
|
620
|
+
payloadJson: serializeBoundedRunEventPayload(value, artifacts),
|
|
621
|
+
artifactsJson,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function normalizeWorkspaceToolRunArtifacts(value) {
|
|
625
|
+
const rawArtifacts = Array.isArray(value.artifacts)
|
|
626
|
+
? value.artifacts
|
|
627
|
+
: value.type === 'artifact' && typeof value.path === 'string'
|
|
628
|
+
? [value]
|
|
629
|
+
: [];
|
|
630
|
+
const artifacts = [];
|
|
631
|
+
for (const raw of rawArtifacts) {
|
|
632
|
+
if (artifacts.length >= WORKSPACE_TOOL_RUN_ARTIFACT_MAX_COUNT)
|
|
633
|
+
break;
|
|
634
|
+
if (!isPlainRecord(raw) || typeof raw.path !== 'string')
|
|
635
|
+
continue;
|
|
636
|
+
const normalizedPath = normalizeWorkspaceToolMediaRelativePath(raw.path);
|
|
637
|
+
if (!normalizedPath)
|
|
638
|
+
continue;
|
|
639
|
+
if (normalizedPath.length > WORKSPACE_TOOL_RUN_ARTIFACT_PATH_MAX_CHARS)
|
|
640
|
+
continue;
|
|
641
|
+
artifacts.push({
|
|
642
|
+
path: normalizedPath,
|
|
643
|
+
label: typeof raw.label === 'string' && raw.label.trim() ? raw.label.trim().slice(0, 200) : undefined,
|
|
644
|
+
mimeType: typeof raw.mimeType === 'string' && raw.mimeType.trim() ? raw.mimeType.trim().slice(0, 200) : undefined,
|
|
645
|
+
sizeBytes: typeof raw.sizeBytes === 'number' && Number.isFinite(raw.sizeBytes) && raw.sizeBytes >= 0
|
|
646
|
+
? Math.floor(raw.sizeBytes)
|
|
647
|
+
: undefined,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
return artifacts;
|
|
651
|
+
}
|
|
652
|
+
function serializeBoundedRunEventArtifacts(artifacts) {
|
|
653
|
+
if (artifacts.length === 0)
|
|
654
|
+
return null;
|
|
655
|
+
const bounded = [];
|
|
656
|
+
for (const artifact of artifacts) {
|
|
657
|
+
const next = [...bounded, artifact];
|
|
658
|
+
const serialized = JSON.stringify(next);
|
|
659
|
+
if (serialized.length > WORKSPACE_TOOL_RUN_ARTIFACTS_MAX_CHARS)
|
|
660
|
+
break;
|
|
661
|
+
bounded.push(artifact);
|
|
662
|
+
}
|
|
663
|
+
return bounded.length > 0 ? JSON.stringify(bounded) : null;
|
|
664
|
+
}
|
|
665
|
+
function serializeBoundedRunEventPayload(value, artifacts) {
|
|
666
|
+
const payloadSource = { ...value };
|
|
667
|
+
delete payloadSource.artifacts;
|
|
668
|
+
if (value.type === 'artifact') {
|
|
669
|
+
delete payloadSource.path;
|
|
670
|
+
delete payloadSource.label;
|
|
671
|
+
delete payloadSource.mimeType;
|
|
672
|
+
delete payloadSource.sizeBytes;
|
|
673
|
+
}
|
|
674
|
+
if (artifacts.length > 0) {
|
|
675
|
+
payloadSource.artifacts = artifacts;
|
|
676
|
+
}
|
|
677
|
+
const payload = scrubInlineBinaryPayload(stripUndefinedValues(payloadSource));
|
|
678
|
+
const serialized = JSON.stringify(payload);
|
|
679
|
+
if (!serialized)
|
|
680
|
+
return null;
|
|
681
|
+
if (serialized.length <= WORKSPACE_TOOL_RUN_EVENT_PAYLOAD_MAX_CHARS)
|
|
682
|
+
return serialized;
|
|
683
|
+
return JSON.stringify({
|
|
684
|
+
type: value.type,
|
|
685
|
+
level: typeof value.level === 'string' ? value.level : undefined,
|
|
686
|
+
summary: typeof value.summary === 'string' ? scrubWorkspaceToolInlineBinaryText(value.summary) : undefined,
|
|
687
|
+
truncated: true,
|
|
688
|
+
preview: scrubInlineBinaryString(serialized.slice(0, WORKSPACE_TOOL_RUN_EVENT_PAYLOAD_MAX_CHARS)),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
export function scrubWorkspaceToolInlineBinaryText(value) {
|
|
692
|
+
return scrubInlineBinaryString(value);
|
|
693
|
+
}
|
|
694
|
+
function scrubInlineBinaryPayload(value) {
|
|
695
|
+
if (typeof value === 'string')
|
|
696
|
+
return scrubInlineBinaryString(value);
|
|
697
|
+
if (Array.isArray(value))
|
|
698
|
+
return value.map((item) => scrubInlineBinaryPayload(item));
|
|
699
|
+
if (!isPlainRecord(value))
|
|
700
|
+
return value;
|
|
701
|
+
const result = {};
|
|
702
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
703
|
+
result[key] = scrubInlineBinaryPayload(raw);
|
|
704
|
+
}
|
|
705
|
+
return result;
|
|
706
|
+
}
|
|
707
|
+
function scrubInlineBinaryString(value) {
|
|
708
|
+
let scrubbed = value.replace(/data:[^,;]+(?:;[^,;]+)*;base64,[a-z0-9+/=\r\n]+/gi, WORKSPACE_TOOL_RUN_EVENT_BASE64_PLACEHOLDER);
|
|
709
|
+
scrubbed = scrubbed.replace(WORKSPACE_TOOL_RUN_EVENT_WRAPPED_BASE64_FRAGMENT_PATTERN, (match, prefix, candidate) => {
|
|
710
|
+
const compact = compactBase64Candidate(String(candidate));
|
|
711
|
+
return compact.length >= WORKSPACE_TOOL_RUN_EVENT_BASE64_MIN_CHARS
|
|
712
|
+
&& compact.length % 4 === 0
|
|
713
|
+
&& /^[A-Za-z0-9+/]+={0,2}$/.test(compact)
|
|
714
|
+
? `${prefix}${WORKSPACE_TOOL_RUN_EVENT_BASE64_PLACEHOLDER}`
|
|
715
|
+
: match;
|
|
716
|
+
});
|
|
717
|
+
scrubbed = scrubbed.replace(WORKSPACE_TOOL_RUN_EVENT_BASE64_FRAGMENT_PATTERN, (match) => {
|
|
718
|
+
const compact = compactBase64Candidate(match);
|
|
719
|
+
return compact.length >= WORKSPACE_TOOL_RUN_EVENT_BASE64_MIN_CHARS
|
|
720
|
+
&& compact.length % 4 === 0
|
|
721
|
+
&& /^[A-Za-z0-9+/]+={0,2}$/.test(compact)
|
|
722
|
+
? WORKSPACE_TOOL_RUN_EVENT_BASE64_PLACEHOLDER
|
|
723
|
+
: match;
|
|
724
|
+
});
|
|
725
|
+
if (scrubbed !== value)
|
|
726
|
+
return scrubbed;
|
|
727
|
+
const compact = compactBase64Candidate(value);
|
|
728
|
+
if (compact.length >= WORKSPACE_TOOL_RUN_EVENT_BASE64_MIN_CHARS
|
|
729
|
+
&& compact.length % 4 === 0
|
|
730
|
+
&& /^[A-Za-z0-9+/]+={0,2}$/.test(compact)) {
|
|
731
|
+
return WORKSPACE_TOOL_RUN_EVENT_BASE64_PLACEHOLDER;
|
|
732
|
+
}
|
|
733
|
+
return value;
|
|
734
|
+
}
|
|
735
|
+
function compactBase64Candidate(value) {
|
|
736
|
+
return value.replace(/(?:\\r|\\n|\\t|\s+)/g, '');
|
|
737
|
+
}
|
|
738
|
+
function stripUndefinedValues(value) {
|
|
739
|
+
if (Array.isArray(value)) {
|
|
740
|
+
return value.map((item) => stripUndefinedValues(item));
|
|
741
|
+
}
|
|
742
|
+
if (!isPlainRecord(value))
|
|
743
|
+
return value;
|
|
744
|
+
const result = {};
|
|
745
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
746
|
+
if (raw === undefined)
|
|
747
|
+
continue;
|
|
748
|
+
result[key] = stripUndefinedValues(raw);
|
|
749
|
+
}
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
export function parseWorkspaceToolRunArtifactRefs(raw) {
|
|
753
|
+
if (!raw)
|
|
754
|
+
return null;
|
|
755
|
+
try {
|
|
756
|
+
const parsed = JSON.parse(raw);
|
|
757
|
+
if (!Array.isArray(parsed))
|
|
758
|
+
return null;
|
|
759
|
+
return parsed.filter((item) => isPlainRecord(item) && typeof item.path === 'string');
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
export function normalizeStateRow(value, options) {
|
|
766
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
767
|
+
return null;
|
|
768
|
+
const record = value;
|
|
769
|
+
const rawFields = isPlainRecord(record.fields) ? record.fields : record;
|
|
770
|
+
const rawMedia = isPlainRecord(record.media) ? record.media : {};
|
|
771
|
+
const media = {};
|
|
772
|
+
for (const [slot, raw] of Object.entries(rawMedia)) {
|
|
773
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
774
|
+
continue;
|
|
775
|
+
const item = raw;
|
|
776
|
+
if (options?.rejectUnsupportedMediaKinds && item.kind !== 'workspace_path') {
|
|
777
|
+
throw new WorkspaceToolServiceError(`Tool state media slot "${slot}" must use kind "workspace_path".`, 400);
|
|
778
|
+
}
|
|
779
|
+
if (item.kind === 'workspace_path' && typeof item.value === 'string') {
|
|
780
|
+
media[slot] = { kind: item.kind, value: item.value };
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return {
|
|
784
|
+
fields: { ...rawFields },
|
|
785
|
+
media,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
export function validateStateRowForManifest(manifest, state) {
|
|
789
|
+
for (const [slotName, media] of Object.entries(state.media ?? {})) {
|
|
790
|
+
if (media.kind !== 'workspace_path') {
|
|
791
|
+
throw new WorkspaceToolServiceError(`Tool state media slot "${slotName}" must use kind "workspace_path".`, 400);
|
|
792
|
+
}
|
|
793
|
+
if (path.isAbsolute(media.value)) {
|
|
794
|
+
throw new WorkspaceToolServiceError(`Tool state media slot "${slotName}" must stay inside the agent workspace.`, 400);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const declaredSlots = new Map((manifest.view.mediaSlots ?? []).map((slot) => [slot.name, slot.kind]));
|
|
798
|
+
for (const [slotName, media] of Object.entries(state.media ?? {})) {
|
|
799
|
+
const expectedKind = declaredSlots.get(slotName);
|
|
800
|
+
if (!expectedKind)
|
|
801
|
+
continue;
|
|
802
|
+
if (media.kind !== expectedKind) {
|
|
803
|
+
throw new WorkspaceToolServiceError(`Tool state media slot "${slotName}" must use kind "${expectedKind}".`, 400);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
export function isPlainRecord(value) {
|
|
808
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
809
|
+
}
|
|
810
|
+
export function serializeJson(value) {
|
|
811
|
+
if (value == null)
|
|
812
|
+
return null;
|
|
813
|
+
return JSON.stringify(value);
|
|
814
|
+
}
|
|
815
|
+
export function isWorkspaceFileNotFoundError(error) {
|
|
816
|
+
const message = String(error?.message ?? error).toLowerCase();
|
|
817
|
+
return message.startsWith('not_found:');
|
|
818
|
+
}
|
|
819
|
+
export function parseJsonRecord(raw) {
|
|
820
|
+
if (!raw)
|
|
821
|
+
return null;
|
|
822
|
+
try {
|
|
823
|
+
const parsed = JSON.parse(raw);
|
|
824
|
+
return isPlainRecord(parsed) ? parsed : null;
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
export function buildDeletedWorkspaceToolSlug(toolId, slug) {
|
|
831
|
+
return `__deleted__/${toolId}/${slug}`;
|
|
832
|
+
}
|
|
833
|
+
export async function waitForTerminalExit(terminalBroker, nodeId, terminalId, options) {
|
|
834
|
+
const deadline = Date.now() + (options?.timeoutMs ?? ONE_SHOT_TIMEOUT_MS);
|
|
835
|
+
while (Date.now() < deadline) {
|
|
836
|
+
const snapshot = await terminalBroker.snapshotTerminal(nodeId, terminalId);
|
|
837
|
+
options?.onSnapshot?.(snapshot);
|
|
838
|
+
if (snapshot.terminal.exited) {
|
|
839
|
+
return snapshot;
|
|
840
|
+
}
|
|
841
|
+
await sleep(ONE_SHOT_POLL_INTERVAL_MS);
|
|
842
|
+
}
|
|
843
|
+
throw new WorkspaceToolServiceError(options?.timeoutMessage ?? 'Timed out waiting for tool action to finish.', 504);
|
|
844
|
+
}
|
|
845
|
+
export function deriveTerminalRunCompletion(source) {
|
|
846
|
+
switch (source.kind) {
|
|
847
|
+
case 'result_event':
|
|
848
|
+
return { status: 'completed', exitCode: 0, signal: null };
|
|
849
|
+
case 'error_event':
|
|
850
|
+
return { status: 'failed', exitCode: source.exitCode ?? null, signal: source.signal ?? null };
|
|
851
|
+
case 'timeout':
|
|
852
|
+
return { status: 'timed_out', exitCode: null, signal: 'timeout' };
|
|
853
|
+
case 'missing_terminal':
|
|
854
|
+
return { status: 'failed', exitCode: null, signal: 'missing_terminal' };
|
|
855
|
+
case 'cancelled':
|
|
856
|
+
return { status: 'cancelled', exitCode: source.exitCode ?? null, signal: source.signal ?? null };
|
|
857
|
+
case 'terminal_exit': {
|
|
858
|
+
const signal = normalizeTerminalExitSignal(source.signal);
|
|
859
|
+
const explicitExitCode = source.exitCode ?? null;
|
|
860
|
+
const exitCode = explicitExitCode ?? (signal == null && !source.disposed ? 0 : null);
|
|
861
|
+
if (signal) {
|
|
862
|
+
return { status: 'cancelled', exitCode, signal };
|
|
863
|
+
}
|
|
864
|
+
if (source.disposed && explicitExitCode == null) {
|
|
865
|
+
return { status: 'cancelled', exitCode: null, signal: null };
|
|
866
|
+
}
|
|
867
|
+
return { status: exitCode === 0 ? 'completed' : 'failed', exitCode, signal: null };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
export function deriveOneShotRunCompletion(snapshot) {
|
|
872
|
+
return deriveTerminalRunCompletion({
|
|
873
|
+
kind: 'terminal_exit',
|
|
874
|
+
exitCode: snapshot.terminal.exitCode,
|
|
875
|
+
signal: snapshot.terminal.signal,
|
|
876
|
+
disposed: false,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
function normalizeTerminalExitSignal(signal) {
|
|
880
|
+
if (signal == null || signal === '0') {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
return signal;
|
|
884
|
+
}
|
|
885
|
+
export async function closeTerminalBestEffort(terminalBroker, nodeId, terminalId) {
|
|
886
|
+
try {
|
|
887
|
+
await terminalBroker.closeTerminal(nodeId, terminalId);
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
890
|
+
// The terminal may already be gone. Cleanup is best-effort.
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
export function sleep(ms) {
|
|
894
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
895
|
+
}
|