@yeaft/webchat-agent 1.0.413 → 1.0.415
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/browser-runtime/browser-install.js +497 -0
- package/browser-runtime/cli.js +88 -0
- package/browser-runtime/config.js +116 -0
- package/browser-runtime/errors.js +8 -0
- package/browser-runtime/extension/manifest.json +18 -0
- package/browser-runtime/extension/offscreen.html +5 -0
- package/browser-runtime/extension/offscreen.js +101 -0
- package/browser-runtime/extension/popup.html +5 -0
- package/browser-runtime/extension/popup.js +1 -0
- package/browser-runtime/extension/service-worker.js +48 -0
- package/browser-runtime/extension.js +45 -0
- package/browser-runtime/index.js +5 -0
- package/browser-runtime/probe.js +427 -0
- package/browser-runtime/protocol.js +71 -0
- package/browser-runtime/service.js +132 -0
- package/browser-runtime/windows-version-job.ps1 +233 -0
- package/browser-runtime/windows-version-worker.js +75 -0
- package/browser-runtime/windows-version.js +85 -0
- package/cli.js +24 -7
- package/connection/index.js +12 -0
- package/context.js +1 -0
- package/index.js +19 -2
- package/llm-config-cli.js +24 -21
- package/local-runtime/server/client-protocol.js +14 -0
- package/local-runtime/server/context.js +3 -2
- package/local-runtime/server/handlers/agent-file-terminal.js +185 -115
- package/local-runtime/server/handlers/agent-output.js +3 -0
- package/local-runtime/server/handlers/client-misc.js +21 -4
- package/local-runtime/server/handlers/client-workbench.js +222 -41
- package/local-runtime/server/workbench-correlation.js +184 -0
- package/local-runtime/server/workbench-route.js +180 -0
- package/local-runtime/server/ws-agent.js +4 -0
- package/local-runtime/server/ws-client.js +25 -3
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +191 -135
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +5 -1
- package/service/config.js +23 -2
- package/service/index.js +1 -0
- package/service/linux.js +3 -2
- package/terminal.js +167 -30
- package/workbench/file-ops.js +21 -20
- package/workbench/file-search.js +4 -3
- package/workbench/git-ops.js +23 -22
- package/workbench/request-routing.js +16 -0
- package/yeaft/cli.js +57 -1
- package/yeaft/config-api.js +138 -192
- package/yeaft/config-store.js +192 -0
- package/yeaft/config.js +3 -0
- package/yeaft/init.js +20 -7
- package/yeaft/sessions/feature-flag.js +15 -33
- package/yeaft/sessions/session-manifest.js +114 -10
- package/yeaft/stdio-protocol.js +57 -0
- package/yeaft/storage/atomic.js +43 -17
- package/yeaft/tools/process-runner.js +86 -13
|
@@ -2,6 +2,7 @@ import { agents, userFileTabs } from '../context.js';
|
|
|
2
2
|
import {
|
|
3
3
|
sendToWebClient, forwardToAgent, broadcastAgentList
|
|
4
4
|
} from '../ws-utils.js';
|
|
5
|
+
import { resolveWorkbenchRequest } from '../workbench-route.js';
|
|
5
6
|
|
|
6
7
|
// Only Agents that explicitly advertise the package-replacement-safe updater
|
|
7
8
|
// may receive remote upgrade commands. Version thresholds are insufficient:
|
|
@@ -73,11 +74,18 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
|
|
|
73
74
|
|
|
74
75
|
// File Tab 状态保存/恢复
|
|
75
76
|
case 'update_file_tabs': {
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
const ftAgentId = msg.agentId || client.currentAgent;
|
|
78
|
+
if (client.userId && ftAgentId) {
|
|
79
|
+
if (!await checkAgentAccess(ftAgentId)) break;
|
|
80
|
+
const resolved = resolveWorkbenchRequest(client, msg, ftAgentId);
|
|
81
|
+
if (!resolved) break;
|
|
82
|
+
const identity = resolved.routeKey
|
|
83
|
+
? `${resolved.routeKey}\u0000${resolved.workspaceGeneration}`
|
|
84
|
+
: ftAgentId;
|
|
85
|
+
const key = `${client.userId}:${identity}`;
|
|
78
86
|
userFileTabs.set(key, {
|
|
79
87
|
files: (msg.openFiles || []).map(f => ({ path: f.path })),
|
|
80
|
-
activeIndex: msg.activeIndex
|
|
88
|
+
activeIndex: Number.isFinite(msg.activeIndex) ? msg.activeIndex : 0,
|
|
81
89
|
timestamp: Date.now()
|
|
82
90
|
});
|
|
83
91
|
}
|
|
@@ -88,10 +96,19 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
|
|
|
88
96
|
const ftAgentId = msg.agentId || client.currentAgent;
|
|
89
97
|
if (client.userId && ftAgentId) {
|
|
90
98
|
if (!await checkAgentAccess(ftAgentId)) break;
|
|
91
|
-
const
|
|
99
|
+
const resolved = resolveWorkbenchRequest(client, msg, ftAgentId);
|
|
100
|
+
if (!resolved) break;
|
|
101
|
+
const identity = resolved.routeKey
|
|
102
|
+
? `${resolved.routeKey}\u0000${resolved.workspaceGeneration}`
|
|
103
|
+
: ftAgentId;
|
|
104
|
+
const key = `${client.userId}:${identity}`;
|
|
92
105
|
const saved = userFileTabs.get(key);
|
|
93
106
|
await sendToWebClient(client, {
|
|
94
107
|
type: 'file_tabs_restored',
|
|
108
|
+
agentId: ftAgentId,
|
|
109
|
+
conversationId: resolved.conversationId || msg.conversationId || client.currentConversation,
|
|
110
|
+
workbenchRouteKey: resolved.routeKey,
|
|
111
|
+
workbenchWorkspaceGeneration: resolved.workspaceGeneration,
|
|
95
112
|
openFiles: saved?.files || [],
|
|
96
113
|
activeIndex: saved?.activeIndex || 0
|
|
97
114
|
});
|
|
@@ -3,6 +3,13 @@ import {
|
|
|
3
3
|
sendToWebClient, forwardToAgent,
|
|
4
4
|
verifyConversationOwnership, getCachedDir
|
|
5
5
|
} from '../ws-utils.js';
|
|
6
|
+
import { resolveWorkbenchRequest } from '../workbench-route.js';
|
|
7
|
+
import {
|
|
8
|
+
deleteWorkbenchRequest,
|
|
9
|
+
getWorkbenchTerminalOwner,
|
|
10
|
+
registerWorkbenchRequest,
|
|
11
|
+
registerWorkbenchTerminalOwner,
|
|
12
|
+
} from '../workbench-correlation.js';
|
|
6
13
|
|
|
7
14
|
/**
|
|
8
15
|
* Handle workbench messages from web client (terminal, file, git operations).
|
|
@@ -25,6 +32,101 @@ function isYeaftVirtualConversation(conversationId) {
|
|
|
25
32
|
return typeof conversationId === 'string' && conversationId.startsWith('yeaft-');
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
async function denyWorkbenchRoute(client, msg) {
|
|
36
|
+
console.warn(`[Security] Invalid Workbench route for ${msg?.type || 'unknown'}`);
|
|
37
|
+
await sendToWebClient(client, { type: 'error', message: 'Invalid Workbench Session route' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const RESPONSE_TYPES = Object.freeze({
|
|
41
|
+
terminal_create: ['terminal_created', 'terminal_error'],
|
|
42
|
+
read_file: ['file_content'],
|
|
43
|
+
write_file: ['file_saved'],
|
|
44
|
+
list_directory: ['directory_listing'],
|
|
45
|
+
git_status: ['git_status_result'],
|
|
46
|
+
git_diff: ['git_diff_result'],
|
|
47
|
+
git_add: ['git_op_result'],
|
|
48
|
+
git_reset: ['git_op_result'],
|
|
49
|
+
git_restore: ['git_op_result'],
|
|
50
|
+
git_commit: ['git_op_result'],
|
|
51
|
+
git_push: ['git_op_result'],
|
|
52
|
+
file_search: ['file_search_result'],
|
|
53
|
+
create_file: ['file_op_result'],
|
|
54
|
+
delete_files: ['file_op_result'],
|
|
55
|
+
move_files: ['file_op_result'],
|
|
56
|
+
copy_files: ['file_op_result'],
|
|
57
|
+
upload_to_dir: ['file_op_result'],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function canonicalWorkbenchMessage(msg, resolved, { canonicalWorkDir = false } = {}) {
|
|
61
|
+
const {
|
|
62
|
+
_requestUserId: _ignoredUserId,
|
|
63
|
+
_requestClientId: _ignoredClientId,
|
|
64
|
+
_workbenchRequestId: _ignoredRequestId,
|
|
65
|
+
...clientFields
|
|
66
|
+
} = msg || {};
|
|
67
|
+
if (resolved.legacy) return {
|
|
68
|
+
...clientFields,
|
|
69
|
+
...(resolved.conversationId ? { conversationId: resolved.conversationId } : {}),
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
...clientFields,
|
|
73
|
+
agentId: resolved.agentId,
|
|
74
|
+
conversationId: resolved.conversationId,
|
|
75
|
+
workDir: canonicalWorkDir ? resolved.workDir : resolved.requestedWorkDir,
|
|
76
|
+
workbenchRoute: resolved.route,
|
|
77
|
+
workbenchRouteKey: resolved.routeKey,
|
|
78
|
+
workbenchWorkspaceGeneration: resolved.workspaceGeneration,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function correlateWorkbenchRequest({ agentId, clientId, client, msg, resolved, canonical }) {
|
|
83
|
+
if (resolved.legacy) {
|
|
84
|
+
return {
|
|
85
|
+
...canonical,
|
|
86
|
+
_requestUserId: client.userId,
|
|
87
|
+
_requestClientId: clientId,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const expectedResponseTypes = RESPONSE_TYPES[msg.type];
|
|
91
|
+
if (!expectedResponseTypes) return canonical;
|
|
92
|
+
const registration = {
|
|
93
|
+
agentId,
|
|
94
|
+
clientId,
|
|
95
|
+
userId: client.userId,
|
|
96
|
+
routeKey: resolved.routeKey,
|
|
97
|
+
conversationId: resolved.conversationId,
|
|
98
|
+
workspaceGeneration: resolved.workspaceGeneration,
|
|
99
|
+
route: resolved.route,
|
|
100
|
+
role: client.role,
|
|
101
|
+
requestType: msg.type,
|
|
102
|
+
expectedResponseTypes,
|
|
103
|
+
publicRequestId: typeof msg.requestId === 'string' ? msg.requestId : null,
|
|
104
|
+
terminalId: msg.terminalId || null,
|
|
105
|
+
};
|
|
106
|
+
const requestId = registerWorkbenchRequest(registration);
|
|
107
|
+
if (!requestId) return null;
|
|
108
|
+
if (msg.type === 'terminal_create'
|
|
109
|
+
&& !registerWorkbenchTerminalOwner({ ...registration, requestId })) {
|
|
110
|
+
deleteWorkbenchRequest({ agentId, requestId });
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return { ...canonical, _workbenchRequestId: requestId };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function forwardCorrelatedWorkbenchRequest({ agentId, clientId, client, msg, resolved, canonical }) {
|
|
117
|
+
const outbound = correlateWorkbenchRequest({ agentId, clientId, client, msg, resolved, canonical });
|
|
118
|
+
if (!outbound) return false;
|
|
119
|
+
try {
|
|
120
|
+
await forwardToAgent(agentId, outbound);
|
|
121
|
+
return true;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (outbound._workbenchRequestId) {
|
|
124
|
+
deleteWorkbenchRequest({ agentId, requestId: outbound._workbenchRequestId });
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
28
130
|
export async function handleClientWorkbench(clientId, client, msg, checkAgentAccess) {
|
|
29
131
|
switch (msg.type) {
|
|
30
132
|
// Terminal messages (forward to agent)
|
|
@@ -35,19 +137,56 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
|
|
|
35
137
|
const termAgentId = msg.agentId || client.currentAgent;
|
|
36
138
|
if (!termAgentId) return;
|
|
37
139
|
if (!await checkAgentAccess(termAgentId)) return;
|
|
38
|
-
const
|
|
140
|
+
const resolved = resolveWorkbenchRequest(client, msg, termAgentId, {
|
|
141
|
+
allowMissingSession: msg.type === 'terminal_close',
|
|
142
|
+
});
|
|
143
|
+
if (!resolved) {
|
|
144
|
+
await denyWorkbenchRoute(client, msg);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const termConvId = resolved.conversationId || msg.conversationId || client.currentConversation;
|
|
39
148
|
if (!termConvId) return;
|
|
40
|
-
if (!CONFIG.skipAuth && !isYeaftVirtualConversation(termConvId) && !verifyConversationOwnership(termConvId, client.userId, client.role)) {
|
|
149
|
+
if (resolved.legacy && !CONFIG.skipAuth && !isYeaftVirtualConversation(termConvId) && !verifyConversationOwnership(termConvId, client.userId, client.role)) {
|
|
41
150
|
console.warn(`[Security] User ${client.userId} terminal access denied for ${termConvId}`);
|
|
42
151
|
await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
|
|
43
152
|
return;
|
|
44
153
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
154
|
+
let terminalResolved = { ...resolved, conversationId: termConvId };
|
|
155
|
+
if (!resolved.legacy && msg.type !== 'terminal_create') {
|
|
156
|
+
const owner = getWorkbenchTerminalOwner(termAgentId, msg.terminalId);
|
|
157
|
+
const ownerMatches = owner
|
|
158
|
+
&& owner.clientId === clientId
|
|
159
|
+
&& owner.userId === client.userId
|
|
160
|
+
&& owner.routeKey === resolved.routeKey
|
|
161
|
+
&& owner.conversationId === termConvId;
|
|
162
|
+
const generationMatches = msg.type === 'terminal_close'
|
|
163
|
+
? msg.workbenchWorkspaceGeneration === owner?.workspaceGeneration
|
|
164
|
+
: owner?.workspaceGeneration === resolved.workspaceGeneration;
|
|
165
|
+
if (!ownerMatches || !generationMatches) {
|
|
166
|
+
await denyWorkbenchRoute(client, msg);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (msg.type === 'terminal_close') {
|
|
170
|
+
terminalResolved = {
|
|
171
|
+
...terminalResolved,
|
|
172
|
+
workspaceGeneration: owner.workspaceGeneration,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const forwarded = await forwardCorrelatedWorkbenchRequest({
|
|
177
|
+
agentId: termAgentId,
|
|
178
|
+
clientId,
|
|
179
|
+
client,
|
|
180
|
+
msg,
|
|
181
|
+
resolved: terminalResolved,
|
|
182
|
+
canonical: canonicalWorkbenchMessage(msg, terminalResolved, {
|
|
183
|
+
canonicalWorkDir: msg.type === 'terminal_create',
|
|
184
|
+
}),
|
|
50
185
|
});
|
|
186
|
+
if (!forwarded) {
|
|
187
|
+
await denyWorkbenchRoute(client, msg);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
51
190
|
break;
|
|
52
191
|
}
|
|
53
192
|
|
|
@@ -55,13 +194,20 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
|
|
|
55
194
|
const fileAgentId = msg.agentId || client.currentAgent;
|
|
56
195
|
if (!fileAgentId) { console.warn('[Server] read_file: no agentId'); return; }
|
|
57
196
|
if (!await checkAgentAccess(fileAgentId)) return;
|
|
58
|
-
const
|
|
197
|
+
const resolved = resolveWorkbenchRequest(client, msg, fileAgentId);
|
|
198
|
+
if (!resolved) {
|
|
199
|
+
await denyWorkbenchRoute(client, msg);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const fileConvId = resolved.conversationId || msg.conversationId || client.currentConversation || '_explorer';
|
|
59
203
|
console.log(`[Server] Forwarding read_file to agent ${fileAgentId}, conv=${fileConvId}, path=${msg.filePath}`);
|
|
60
|
-
await
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
204
|
+
await forwardCorrelatedWorkbenchRequest({
|
|
205
|
+
agentId: fileAgentId,
|
|
206
|
+
clientId,
|
|
207
|
+
client,
|
|
208
|
+
msg,
|
|
209
|
+
resolved,
|
|
210
|
+
canonical: canonicalWorkbenchMessage(msg, { ...resolved, conversationId: fileConvId }),
|
|
65
211
|
});
|
|
66
212
|
break;
|
|
67
213
|
}
|
|
@@ -70,20 +216,27 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
|
|
|
70
216
|
const writeAgentId = msg.agentId || client.currentAgent;
|
|
71
217
|
if (!writeAgentId) return;
|
|
72
218
|
if (!await checkAgentAccess(writeAgentId)) return;
|
|
73
|
-
const
|
|
219
|
+
const resolved = resolveWorkbenchRequest(client, msg, writeAgentId);
|
|
220
|
+
if (!resolved) {
|
|
221
|
+
await denyWorkbenchRoute(client, msg);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const writeConvId = resolved.conversationId || msg.conversationId || client.currentConversation || '_explorer';
|
|
74
225
|
const isAgentLevelWrite = writeConvId.startsWith('_') || isYeaftVirtualConversation(writeConvId);
|
|
75
|
-
if (!isAgentLevelWrite) {
|
|
226
|
+
if (resolved.legacy && !isAgentLevelWrite) {
|
|
76
227
|
if (!CONFIG.skipAuth && !verifyConversationOwnership(writeConvId, client.userId, client.role)) {
|
|
77
228
|
console.warn(`[Security] User ${client.userId} file write denied for ${writeConvId}`);
|
|
78
229
|
await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
|
|
79
230
|
return;
|
|
80
231
|
}
|
|
81
232
|
}
|
|
82
|
-
await
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
233
|
+
await forwardCorrelatedWorkbenchRequest({
|
|
234
|
+
agentId: writeAgentId,
|
|
235
|
+
clientId,
|
|
236
|
+
client,
|
|
237
|
+
msg,
|
|
238
|
+
resolved,
|
|
239
|
+
canonical: canonicalWorkbenchMessage(msg, { ...resolved, conversationId: writeConvId }),
|
|
87
240
|
});
|
|
88
241
|
break;
|
|
89
242
|
}
|
|
@@ -93,28 +246,40 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
|
|
|
93
246
|
if (!dirAgentId) return;
|
|
94
247
|
if (!await checkAgentAccess(dirAgentId)) return;
|
|
95
248
|
|
|
96
|
-
|
|
97
|
-
|
|
249
|
+
const resolved = resolveWorkbenchRequest(client, msg, dirAgentId);
|
|
250
|
+
if (!resolved) {
|
|
251
|
+
await denyWorkbenchRoute(client, msg);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const canonical = canonicalWorkbenchMessage(msg, {
|
|
255
|
+
...resolved,
|
|
256
|
+
conversationId: resolved.conversationId || msg.conversationId || client.currentConversation || '_explorer',
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Route-scoped requests bypass the legacy Agent/path cache. Relative
|
|
260
|
+
// paths can mean different directories in sibling Sessions.
|
|
261
|
+
const cached = resolved.legacy ? getCachedDir(dirAgentId, canonical.dirPath) : null;
|
|
98
262
|
if (cached) {
|
|
99
263
|
await sendToWebClient(client, {
|
|
100
264
|
type: 'directory_listing',
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
265
|
+
agentId: dirAgentId,
|
|
266
|
+
conversationId: canonical.conversationId,
|
|
267
|
+
requestId: canonical.requestId,
|
|
268
|
+
workbenchRouteKey: canonical.workbenchRouteKey,
|
|
269
|
+
dirPath: canonical.dirPath,
|
|
104
270
|
entries: cached,
|
|
105
271
|
fromCache: true
|
|
106
272
|
});
|
|
107
273
|
return;
|
|
108
274
|
}
|
|
109
275
|
|
|
110
|
-
await
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
_requestClientId: clientId
|
|
276
|
+
await forwardCorrelatedWorkbenchRequest({
|
|
277
|
+
agentId: dirAgentId,
|
|
278
|
+
clientId,
|
|
279
|
+
client,
|
|
280
|
+
msg,
|
|
281
|
+
resolved,
|
|
282
|
+
canonical: { ...canonical, type: 'list_directory' },
|
|
118
283
|
});
|
|
119
284
|
break;
|
|
120
285
|
}
|
|
@@ -130,10 +295,18 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
|
|
|
130
295
|
const gitAgentId = msg.agentId || client.currentAgent;
|
|
131
296
|
if (!gitAgentId) return;
|
|
132
297
|
if (!await checkAgentAccess(gitAgentId)) return;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
298
|
+
const resolved = resolveWorkbenchRequest(client, msg, gitAgentId);
|
|
299
|
+
if (!resolved) {
|
|
300
|
+
await denyWorkbenchRoute(client, msg);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
await forwardCorrelatedWorkbenchRequest({
|
|
304
|
+
agentId: gitAgentId,
|
|
305
|
+
clientId,
|
|
306
|
+
client,
|
|
307
|
+
msg,
|
|
308
|
+
resolved,
|
|
309
|
+
canonical: canonicalWorkbenchMessage(msg, resolved),
|
|
137
310
|
});
|
|
138
311
|
break;
|
|
139
312
|
}
|
|
@@ -146,10 +319,18 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
|
|
|
146
319
|
const fopAgentId = msg.agentId || client.currentAgent;
|
|
147
320
|
if (!fopAgentId) return;
|
|
148
321
|
if (!await checkAgentAccess(fopAgentId)) return;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
322
|
+
const resolved = resolveWorkbenchRequest(client, msg, fopAgentId);
|
|
323
|
+
if (!resolved) {
|
|
324
|
+
await denyWorkbenchRoute(client, msg);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
await forwardCorrelatedWorkbenchRequest({
|
|
328
|
+
agentId: fopAgentId,
|
|
329
|
+
clientId,
|
|
330
|
+
client,
|
|
331
|
+
msg,
|
|
332
|
+
resolved,
|
|
333
|
+
canonical: canonicalWorkbenchMessage(msg, resolved),
|
|
153
334
|
});
|
|
154
335
|
break;
|
|
155
336
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const REQUEST_TTL_MS = 2 * 60 * 1000;
|
|
4
|
+
const MAX_PENDING = 4096;
|
|
5
|
+
|
|
6
|
+
const pendingRequests = new Map();
|
|
7
|
+
const terminalOwners = new Map();
|
|
8
|
+
|
|
9
|
+
function requestKey(agentId, requestId) {
|
|
10
|
+
return `${String(agentId || '')}\u0000${String(requestId || '')}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function terminalKey(agentId, terminalId) {
|
|
14
|
+
return `${String(agentId || '')}\u0000${String(terminalId || '')}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function releasePendingTerminalReservation(pending) {
|
|
18
|
+
if (pending?.timeout) clearTimeout(pending.timeout);
|
|
19
|
+
if (!pending?.terminalId) return;
|
|
20
|
+
const key = terminalKey(pending.agentId, pending.terminalId);
|
|
21
|
+
const owner = terminalOwners.get(key);
|
|
22
|
+
if (owner?.pendingRequestId === pending.requestId) terminalOwners.delete(key);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function prune(now = Date.now()) {
|
|
26
|
+
for (const [key, pending] of pendingRequests) {
|
|
27
|
+
if (pending && pending.expiresAt > now) continue;
|
|
28
|
+
pendingRequests.delete(key);
|
|
29
|
+
releasePendingTerminalReservation(pending);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function registerWorkbenchRequest({
|
|
34
|
+
agentId,
|
|
35
|
+
clientId,
|
|
36
|
+
userId,
|
|
37
|
+
routeKey,
|
|
38
|
+
conversationId,
|
|
39
|
+
workspaceGeneration,
|
|
40
|
+
route,
|
|
41
|
+
role = null,
|
|
42
|
+
requestType,
|
|
43
|
+
expectedResponseTypes,
|
|
44
|
+
publicRequestId = null,
|
|
45
|
+
terminalId = null,
|
|
46
|
+
}) {
|
|
47
|
+
if (!agentId || !clientId || !userId || !routeKey || !conversationId
|
|
48
|
+
|| !workspaceGeneration || !requestType) return null;
|
|
49
|
+
const expected = Array.isArray(expectedResponseTypes)
|
|
50
|
+
? expectedResponseTypes.filter(Boolean)
|
|
51
|
+
: [];
|
|
52
|
+
if (expected.length === 0) return null;
|
|
53
|
+
prune();
|
|
54
|
+
while (pendingRequests.size >= MAX_PENDING) {
|
|
55
|
+
const oldest = pendingRequests.keys().next().value;
|
|
56
|
+
if (oldest == null) break;
|
|
57
|
+
const evicted = pendingRequests.get(oldest);
|
|
58
|
+
pendingRequests.delete(oldest);
|
|
59
|
+
releasePendingTerminalReservation(evicted);
|
|
60
|
+
}
|
|
61
|
+
const requestId = randomUUID();
|
|
62
|
+
const key = requestKey(agentId, requestId);
|
|
63
|
+
const pending = {
|
|
64
|
+
agentId,
|
|
65
|
+
requestId,
|
|
66
|
+
clientId,
|
|
67
|
+
userId,
|
|
68
|
+
routeKey,
|
|
69
|
+
conversationId,
|
|
70
|
+
workspaceGeneration,
|
|
71
|
+
route: route ? { ...route } : null,
|
|
72
|
+
role,
|
|
73
|
+
requestType,
|
|
74
|
+
expectedResponseTypes: new Set(expected),
|
|
75
|
+
publicRequestId,
|
|
76
|
+
terminalId,
|
|
77
|
+
expiresAt: Date.now() + REQUEST_TTL_MS,
|
|
78
|
+
timeout: null,
|
|
79
|
+
};
|
|
80
|
+
pending.timeout = setTimeout(() => {
|
|
81
|
+
const current = pendingRequests.get(key);
|
|
82
|
+
if (current !== pending) return;
|
|
83
|
+
pendingRequests.delete(key);
|
|
84
|
+
releasePendingTerminalReservation(pending);
|
|
85
|
+
}, REQUEST_TTL_MS);
|
|
86
|
+
pending.timeout.unref?.();
|
|
87
|
+
pendingRequests.set(key, pending);
|
|
88
|
+
return requestId;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function consumeWorkbenchRequest({ agentId, requestId, responseType, routeKey = null }) {
|
|
92
|
+
if (!agentId || !requestId || !responseType) return null;
|
|
93
|
+
prune();
|
|
94
|
+
const key = requestKey(agentId, requestId);
|
|
95
|
+
const pending = pendingRequests.get(key);
|
|
96
|
+
if (!pending || !pending.expectedResponseTypes.has(responseType)) return null;
|
|
97
|
+
if (routeKey && pending.routeKey !== routeKey) return null;
|
|
98
|
+
pendingRequests.delete(key);
|
|
99
|
+
if (pending.timeout) clearTimeout(pending.timeout);
|
|
100
|
+
pending.timeout = null;
|
|
101
|
+
return pending;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function deleteWorkbenchRequest({ agentId, requestId }) {
|
|
105
|
+
const key = requestKey(agentId, requestId);
|
|
106
|
+
const pending = pendingRequests.get(key);
|
|
107
|
+
const deleted = pendingRequests.delete(key);
|
|
108
|
+
if (deleted) releasePendingTerminalReservation(pending);
|
|
109
|
+
return deleted;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function registerWorkbenchTerminalOwner(pending) {
|
|
113
|
+
if (!pending?.agentId || !pending?.terminalId || !pending?.clientId
|
|
114
|
+
|| !pending?.userId || !pending?.routeKey || !pending?.workspaceGeneration) return false;
|
|
115
|
+
const key = terminalKey(pending.agentId, pending.terminalId);
|
|
116
|
+
const existing = terminalOwners.get(key);
|
|
117
|
+
if (existing && (
|
|
118
|
+
existing.clientId !== pending.clientId
|
|
119
|
+
|| existing.userId !== pending.userId
|
|
120
|
+
|| existing.routeKey !== pending.routeKey
|
|
121
|
+
|| existing.workspaceGeneration !== pending.workspaceGeneration
|
|
122
|
+
|| existing.conversationId !== pending.conversationId
|
|
123
|
+
)) return false;
|
|
124
|
+
terminalOwners.set(key, {
|
|
125
|
+
agentId: pending.agentId,
|
|
126
|
+
terminalId: pending.terminalId,
|
|
127
|
+
clientId: pending.clientId,
|
|
128
|
+
userId: pending.userId,
|
|
129
|
+
routeKey: pending.routeKey,
|
|
130
|
+
conversationId: pending.conversationId,
|
|
131
|
+
workspaceGeneration: pending.workspaceGeneration,
|
|
132
|
+
pendingRequestId: pending.requestId || pending.pendingRequestId || null,
|
|
133
|
+
});
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getWorkbenchTerminalOwner(agentId, terminalId) {
|
|
138
|
+
return terminalOwners.get(terminalKey(agentId, terminalId)) || null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function deleteWorkbenchTerminalOwner(agentId, terminalId) {
|
|
142
|
+
return terminalOwners.delete(terminalKey(agentId, terminalId));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function clearWorkbenchCorrelationsForClient(clientId) {
|
|
146
|
+
if (!clientId) return [];
|
|
147
|
+
for (const [key, pending] of pendingRequests) {
|
|
148
|
+
if (pending?.clientId !== clientId) continue;
|
|
149
|
+
pendingRequests.delete(key);
|
|
150
|
+
releasePendingTerminalReservation(pending);
|
|
151
|
+
}
|
|
152
|
+
const terminals = [];
|
|
153
|
+
for (const [key, owner] of terminalOwners) {
|
|
154
|
+
if (owner?.clientId !== clientId) continue;
|
|
155
|
+
terminals.push(owner);
|
|
156
|
+
terminalOwners.delete(key);
|
|
157
|
+
}
|
|
158
|
+
return terminals;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function clearWorkbenchCorrelationsForAgent(agentId) {
|
|
162
|
+
if (!agentId) return;
|
|
163
|
+
for (const [key, pending] of pendingRequests) {
|
|
164
|
+
if (pending?.agentId !== agentId) continue;
|
|
165
|
+
pendingRequests.delete(key);
|
|
166
|
+
releasePendingTerminalReservation(pending);
|
|
167
|
+
}
|
|
168
|
+
for (const [key, owner] of terminalOwners) {
|
|
169
|
+
if (owner?.agentId === agentId) terminalOwners.delete(key);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function __testResetWorkbenchCorrelations() {
|
|
174
|
+
for (const pending of pendingRequests.values()) releasePendingTerminalReservation(pending);
|
|
175
|
+
pendingRequests.clear();
|
|
176
|
+
terminalOwners.clear();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function __testExpireWorkbenchRequest(agentId, requestId) {
|
|
180
|
+
const pending = pendingRequests.get(requestKey(agentId, requestId));
|
|
181
|
+
if (!pending) return false;
|
|
182
|
+
pending.expiresAt = 0;
|
|
183
|
+
return true;
|
|
184
|
+
}
|