@yeaft/webchat-agent 1.0.268 → 1.0.270
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/connection/index.js +12 -2
- package/connection/message-router.js +15 -13
- package/local-runtime/server/database.js +1 -0
- package/local-runtime/server/db/connection.js +48 -0
- package/local-runtime/server/db/session-db.js +5 -0
- package/local-runtime/server/db/session-ui-metadata-db.js +78 -0
- package/local-runtime/server/db/yeaft-session-db.js +22 -11
- package/local-runtime/server/handlers/agent-output.js +7 -1
- package/local-runtime/server/handlers/client-conversation.js +232 -58
- package/local-runtime/server/handlers/client-workbench.js +2 -2
- package/local-runtime/server/handlers/session-pin-router.js +7 -6
- package/local-runtime/server/session-catalog.js +91 -0
- package/local-runtime/server/ws-utils.js +78 -5
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +185 -85
- 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 +1 -1
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import { CONFIG } from '../config.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
sessionDb,
|
|
5
|
+
messageDb,
|
|
6
|
+
userDb,
|
|
7
|
+
yeaftSessionDb,
|
|
8
|
+
sessionUiMetadataDb,
|
|
9
|
+
} from '../database.js';
|
|
4
10
|
import { agents, pendingFiles, trackUserTurn, webClients } from '../context.js';
|
|
5
11
|
import {
|
|
6
12
|
sendToWebClient, forwardToAgent,
|
|
7
|
-
broadcastAgentList,
|
|
13
|
+
broadcastAgentList, broadcastSessionCatalog,
|
|
14
|
+
verifyConversationOwnership, verifyAgentOwnership
|
|
8
15
|
} from '../ws-utils.js';
|
|
9
16
|
import { routeSessionPin } from './session-pin-router.js';
|
|
10
17
|
import { recordPerfTraceEvent } from '../perf-trace.js';
|
|
18
|
+
import {
|
|
19
|
+
chatCatalogKey,
|
|
20
|
+
yeaftCatalogKey,
|
|
21
|
+
} from '../session-catalog.js';
|
|
11
22
|
|
|
12
23
|
|
|
13
24
|
function isRetiredCollabSessionId(id) {
|
|
@@ -33,6 +44,22 @@ async function broadcastSessionPin(userId, payload) {
|
|
|
33
44
|
}
|
|
34
45
|
}
|
|
35
46
|
|
|
47
|
+
function persistSessionPin(userId, routeRef, pinned) {
|
|
48
|
+
const { runtimeProvider, agentId, sessionId } = routeRef;
|
|
49
|
+
const catalogKey = runtimeProvider === 'yeaft'
|
|
50
|
+
? yeaftCatalogKey(agentId, sessionId)
|
|
51
|
+
: chatCatalogKey(sessionId);
|
|
52
|
+
const current = sessionUiMetadataDb.get(userId, catalogKey);
|
|
53
|
+
return sessionUiMetadataDb.applyBatch(userId, [{
|
|
54
|
+
catalogKey,
|
|
55
|
+
runtimeProvider,
|
|
56
|
+
agentId,
|
|
57
|
+
sessionId,
|
|
58
|
+
pinned,
|
|
59
|
+
sortRank: current?.sortRank ?? null,
|
|
60
|
+
}]);
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
export function groupOnlineYeaftSessions(rows, agentRegistry = agents) {
|
|
37
64
|
const byAgent = {};
|
|
38
65
|
for (const row of Array.isArray(rows) ? rows : []) {
|
|
@@ -289,44 +316,58 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
289
316
|
}
|
|
290
317
|
|
|
291
318
|
case 'delete_conversation': {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
console.warn(`[Security] User ${client.userId} attempted to delete conversation ${msg.conversationId}
|
|
299
|
-
await sendToWebClient(client, {
|
|
319
|
+
const persisted = sessionDb.get(msg.conversationId);
|
|
320
|
+
const deleteAgentId = msg.agentId || persisted?.agent_id || client.currentAgent;
|
|
321
|
+
if (!deleteAgentId) return;
|
|
322
|
+
if (!CONFIG.skipAuth && (!verifyConversationOwnership(msg.conversationId, client.userId, client.role)
|
|
323
|
+
|| (agents.has(deleteAgentId) && !verifyAgentOwnership(deleteAgentId, client.userId, client.role))
|
|
324
|
+
|| (persisted?.agent_id && persisted.agent_id !== deleteAgentId))) {
|
|
325
|
+
console.warn(`[Security] User ${client.userId} attempted to delete conversation ${msg.conversationId} on agent ${deleteAgentId}`);
|
|
326
|
+
await sendToWebClient(client, {
|
|
327
|
+
type: 'conversation_delete_result',
|
|
328
|
+
requestId: msg.requestId || null,
|
|
329
|
+
conversationId: msg.conversationId,
|
|
330
|
+
agentId: deleteAgentId,
|
|
331
|
+
ok: false,
|
|
332
|
+
error: 'Permission denied',
|
|
333
|
+
});
|
|
300
334
|
return;
|
|
301
335
|
}
|
|
302
336
|
|
|
303
|
-
// Always deactivate in DB — this is the critical fix
|
|
304
337
|
try {
|
|
305
338
|
sessionDb.setActive(msg.conversationId, false);
|
|
339
|
+
const deleteAgent = agents.get(deleteAgentId);
|
|
340
|
+
deleteAgent?.conversations.delete(msg.conversationId);
|
|
341
|
+
await broadcastAgentList();
|
|
342
|
+
if (deleteAgent?.ws?.readyState === 1) {
|
|
343
|
+
await forwardToAgent(deleteAgentId, {
|
|
344
|
+
type: 'delete_conversation',
|
|
345
|
+
conversationId: msg.conversationId,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
await sendToWebClient(client, {
|
|
349
|
+
type: 'conversation_delete_result',
|
|
350
|
+
requestId: msg.requestId || null,
|
|
351
|
+
conversationId: msg.conversationId,
|
|
352
|
+
agentId: deleteAgentId,
|
|
353
|
+
ok: true,
|
|
354
|
+
});
|
|
306
355
|
} catch (e) {
|
|
307
356
|
console.error('Failed to deactivate session in database:', e.message);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
await broadcastAgentList();
|
|
316
|
-
|
|
317
|
-
// Forward to agent for resource cleanup (terminals, processes, etc.) — best effort
|
|
318
|
-
// Only attempt if agent is online with an open WebSocket
|
|
319
|
-
if (deleteAgent?.ws?.readyState === 1) {
|
|
320
|
-
await forwardToAgent(client.currentAgent, {
|
|
321
|
-
type: 'delete_conversation',
|
|
322
|
-
conversationId: msg.conversationId
|
|
357
|
+
await sendToWebClient(client, {
|
|
358
|
+
type: 'conversation_delete_result',
|
|
359
|
+
requestId: msg.requestId || null,
|
|
360
|
+
conversationId: msg.conversationId,
|
|
361
|
+
agentId: deleteAgentId,
|
|
362
|
+
ok: false,
|
|
363
|
+
error: e.message,
|
|
323
364
|
});
|
|
324
365
|
}
|
|
325
366
|
break;
|
|
326
367
|
}
|
|
327
368
|
|
|
328
369
|
case 'select_conversation':
|
|
329
|
-
if (!CONFIG.skipAuth && !verifyConversationOwnership(msg.conversationId, client.userId)) {
|
|
370
|
+
if (!CONFIG.skipAuth && !verifyConversationOwnership(msg.conversationId, client.userId, client.role)) {
|
|
330
371
|
console.warn(`[Security] User ${client.userId} attempted to select conversation ${msg.conversationId} they don't own`);
|
|
331
372
|
await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
|
|
332
373
|
return;
|
|
@@ -361,6 +402,7 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
361
402
|
} catch (e) {
|
|
362
403
|
console.warn('[Server] yeaftSessionDb reorder failed:', e?.message || e);
|
|
363
404
|
}
|
|
405
|
+
if (ok) await broadcastSessionCatalog(client.userId);
|
|
364
406
|
await sendToWebClient(client, {
|
|
365
407
|
type: 'session_crud_result',
|
|
366
408
|
op: 'reorder',
|
|
@@ -371,6 +413,109 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
371
413
|
break;
|
|
372
414
|
}
|
|
373
415
|
|
|
416
|
+
case 'reorder_session_catalog': {
|
|
417
|
+
if (!client.userId || !Array.isArray(msg.sessions)) break;
|
|
418
|
+
const updates = [];
|
|
419
|
+
for (const [index, item] of msg.sessions.entries()) {
|
|
420
|
+
const routeRef = item?.routeRef;
|
|
421
|
+
if (!item?.catalogKey || !routeRef?.runtimeProvider) { updates.length = 0; break; }
|
|
422
|
+
const { runtimeProvider, agentId, sessionId } = routeRef;
|
|
423
|
+
let expectedCatalogKey = null;
|
|
424
|
+
if (runtimeProvider === 'yeaft') {
|
|
425
|
+
if (!agentId || !sessionId || !yeaftSessionDb.getForAgent(client.userId, agentId, sessionId)) {
|
|
426
|
+
updates.length = 0;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
expectedCatalogKey = yeaftCatalogKey(agentId, sessionId);
|
|
430
|
+
} else if (runtimeProvider === 'claude-code' || runtimeProvider === 'copilot') {
|
|
431
|
+
if (!agentId || !sessionId
|
|
432
|
+
|| (!CONFIG.skipAuth && !verifyConversationOwnership(sessionId, client.userId, client.role))) {
|
|
433
|
+
updates.length = 0;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
const row = sessionDb.get(sessionId);
|
|
437
|
+
if (!row || row.agent_id !== agentId || (row.provider || 'claude-code') !== runtimeProvider) {
|
|
438
|
+
updates.length = 0;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
expectedCatalogKey = chatCatalogKey(sessionId);
|
|
442
|
+
}
|
|
443
|
+
if (expectedCatalogKey !== item.catalogKey) { updates.length = 0; break; }
|
|
444
|
+
updates.push({
|
|
445
|
+
catalogKey: expectedCatalogKey,
|
|
446
|
+
runtimeProvider,
|
|
447
|
+
agentId,
|
|
448
|
+
sessionId,
|
|
449
|
+
pinned: item.pinned === true,
|
|
450
|
+
sortRank: index,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
let persisted = false;
|
|
454
|
+
if (updates.length === msg.sessions.length && updates.length > 0) {
|
|
455
|
+
try {
|
|
456
|
+
persisted = sessionUiMetadataDb.applyBatch(client.userId, updates);
|
|
457
|
+
if (persisted) await broadcastSessionCatalog(client.userId);
|
|
458
|
+
} catch (e) {
|
|
459
|
+
console.warn('[Server] Session catalog reorder failed:', e?.message || e);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
await sendToWebClient(client, {
|
|
463
|
+
type: 'session_catalog_reorder_result',
|
|
464
|
+
requestId: msg.requestId || null,
|
|
465
|
+
ok: persisted,
|
|
466
|
+
...(!persisted ? { error: 'Permission denied or stale Session route' } : {}),
|
|
467
|
+
});
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
case 'set_session_ui_metadata': {
|
|
472
|
+
if (!client.userId || !msg.catalogKey || !msg.routeRef?.runtimeProvider) break;
|
|
473
|
+
const { runtimeProvider, agentId, sessionId } = msg.routeRef;
|
|
474
|
+
let expectedCatalogKey = null;
|
|
475
|
+
if (runtimeProvider === 'yeaft') {
|
|
476
|
+
if (agentId && sessionId && yeaftSessionDb.getForAgent(client.userId, agentId, sessionId)) {
|
|
477
|
+
expectedCatalogKey = yeaftCatalogKey(agentId, sessionId);
|
|
478
|
+
}
|
|
479
|
+
} else if (runtimeProvider === 'claude-code' || runtimeProvider === 'copilot') {
|
|
480
|
+
const row = sessionId ? sessionDb.get(sessionId) : null;
|
|
481
|
+
if (agentId && sessionId
|
|
482
|
+
&& (CONFIG.skipAuth || verifyConversationOwnership(sessionId, client.userId, client.role))
|
|
483
|
+
&& row?.agent_id === agentId
|
|
484
|
+
&& (row.provider || 'claude-code') === runtimeProvider) {
|
|
485
|
+
expectedCatalogKey = chatCatalogKey(sessionId);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const authorized = expectedCatalogKey === msg.catalogKey;
|
|
489
|
+
let persisted = false;
|
|
490
|
+
if (authorized) {
|
|
491
|
+
try {
|
|
492
|
+
persisted = sessionUiMetadataDb.applyBatch(client.userId, [{
|
|
493
|
+
catalogKey: expectedCatalogKey,
|
|
494
|
+
runtimeProvider,
|
|
495
|
+
agentId,
|
|
496
|
+
sessionId,
|
|
497
|
+
pinned: msg.pinned === true,
|
|
498
|
+
sortRank: Number.isFinite(msg.sortRank) ? msg.sortRank : null,
|
|
499
|
+
}]);
|
|
500
|
+
if (persisted) await broadcastSessionCatalog(client.userId);
|
|
501
|
+
} catch (e) {
|
|
502
|
+
console.warn('[Server] Session metadata update failed:', e?.message || e);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
await sendToWebClient(client, {
|
|
506
|
+
type: 'session_ui_metadata_updated',
|
|
507
|
+
requestId: msg.requestId || null,
|
|
508
|
+
ok: persisted,
|
|
509
|
+
catalogKey: msg.catalogKey,
|
|
510
|
+
routeRef: msg.routeRef,
|
|
511
|
+
...(persisted ? {
|
|
512
|
+
pinned: msg.pinned === true,
|
|
513
|
+
sortRank: Number.isFinite(msg.sortRank) ? msg.sortRank : null,
|
|
514
|
+
} : { error: 'Permission denied or stale Session route' }),
|
|
515
|
+
});
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
|
|
374
519
|
case 'pin_session':
|
|
375
520
|
case 'unpin_session': {
|
|
376
521
|
// fix-yeaft-session-list-and-menu: yeaft sessions live in a
|
|
@@ -388,22 +533,17 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
388
533
|
}
|
|
389
534
|
const isPinned = msg.type === 'pin_session';
|
|
390
535
|
try {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
workDir: msg.workDir || '',
|
|
398
|
-
},
|
|
399
|
-
isPinned,
|
|
400
|
-
);
|
|
401
|
-
if (!ok) {
|
|
536
|
+
const row = yeaftSessionDb.getForAgent(client.userId, explicitYeaftAgentId, msg.conversationId);
|
|
537
|
+
if (!row || !persistSessionPin(client.userId, {
|
|
538
|
+
runtimeProvider: 'yeaft',
|
|
539
|
+
agentId: explicitYeaftAgentId,
|
|
540
|
+
sessionId: msg.conversationId,
|
|
541
|
+
}, isPinned)) {
|
|
402
542
|
console.warn(`[Server] Unauthorized yeaft pin ${msg.conversationId} by ${client.userId}`);
|
|
403
543
|
break;
|
|
404
544
|
}
|
|
405
545
|
} catch (e) {
|
|
406
|
-
console.warn(`[Server]
|
|
546
|
+
console.warn(`[Server] Yeaft Session pin failed for ${msg.conversationId}:`, e?.message || e);
|
|
407
547
|
break;
|
|
408
548
|
}
|
|
409
549
|
await broadcastSessionPin(client.userId, {
|
|
@@ -413,13 +553,14 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
413
553
|
sessionKind: 'yeaft',
|
|
414
554
|
pinned: isPinned,
|
|
415
555
|
});
|
|
556
|
+
await broadcastSessionCatalog(client.userId);
|
|
416
557
|
break;
|
|
417
558
|
}
|
|
418
559
|
|
|
419
560
|
const route = routeSessionPin(
|
|
420
561
|
{
|
|
421
|
-
|
|
422
|
-
verifyChatOwnership: (id, userId) => verifyConversationOwnership(id, userId),
|
|
562
|
+
getYeaftRows: (id) => yeaftSessionDb.getAllById(id),
|
|
563
|
+
verifyChatOwnership: (id, userId) => verifyConversationOwnership(id, userId, client.role),
|
|
423
564
|
skipAuth: CONFIG.skipAuth,
|
|
424
565
|
},
|
|
425
566
|
client,
|
|
@@ -433,24 +574,45 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
433
574
|
break;
|
|
434
575
|
}
|
|
435
576
|
if (route.kind === 'yeaft') {
|
|
436
|
-
try {
|
|
437
|
-
|
|
438
|
-
|
|
577
|
+
try {
|
|
578
|
+
persistSessionPin(client.userId, {
|
|
579
|
+
runtimeProvider: 'yeaft',
|
|
580
|
+
agentId: route.agentId,
|
|
581
|
+
sessionId: route.id,
|
|
582
|
+
}, route.isPinned);
|
|
583
|
+
} catch (e) {
|
|
584
|
+
console.warn(`[Server] Yeaft Session pin failed for ${route.id}:`, e?.message || e);
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
await sendToWebClient(client, {
|
|
588
|
+
type: 'session_pinned',
|
|
589
|
+
conversationId: route.id,
|
|
590
|
+
agentId: route.agentId,
|
|
591
|
+
sessionKind: 'yeaft',
|
|
592
|
+
pinned: route.isPinned,
|
|
593
|
+
});
|
|
594
|
+
await broadcastSessionCatalog(client.userId);
|
|
439
595
|
break;
|
|
440
596
|
}
|
|
441
597
|
// route.kind === 'chat'
|
|
442
598
|
try {
|
|
443
|
-
sessionDb.
|
|
599
|
+
const row = sessionDb.get(route.id);
|
|
600
|
+
persistSessionPin(client.userId, {
|
|
601
|
+
runtimeProvider: row?.provider || 'claude-code',
|
|
602
|
+
agentId: row?.agent_id || null,
|
|
603
|
+
sessionId: route.id,
|
|
604
|
+
}, route.isPinned);
|
|
444
605
|
// If pinning, also ensure session is active (reactivate if it was auto-deactivated)
|
|
445
606
|
if (route.isPinned) sessionDb.setActive(route.id, true);
|
|
446
|
-
} catch (e) {
|
|
607
|
+
} catch (e) { break; }
|
|
447
608
|
await sendToWebClient(client, { type: 'session_pinned', conversationId: route.id, pinned: route.isPinned });
|
|
609
|
+
await broadcastSessionCatalog(client.userId);
|
|
448
610
|
break;
|
|
449
611
|
}
|
|
450
612
|
|
|
451
613
|
case 'sync_messages':
|
|
452
614
|
if (msg.conversationId) {
|
|
453
|
-
if (!CONFIG.skipAuth && !verifyConversationOwnership(msg.conversationId, client.userId)) {
|
|
615
|
+
if (!CONFIG.skipAuth && !verifyConversationOwnership(msg.conversationId, client.userId, client.role)) {
|
|
454
616
|
console.warn(`[Security] User ${client.userId} attempted to sync messages for conversation ${msg.conversationId} they don't own`);
|
|
455
617
|
return;
|
|
456
618
|
}
|
|
@@ -486,9 +648,17 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
486
648
|
|
|
487
649
|
const total = messageDb.getCount(msg.conversationId);
|
|
488
650
|
console.log(`[sync_messages] Found ${messages.length} messages (total=${total}, hasMore=${hasMore})`);
|
|
651
|
+
const mode = msg.afterMessageId !== undefined && msg.afterMessageId !== null
|
|
652
|
+
? 'delta'
|
|
653
|
+
: (msg.beforeId ? 'older' : 'recent');
|
|
489
654
|
await sendToWebClient(client, {
|
|
490
655
|
type: 'sync_messages_result',
|
|
491
656
|
conversationId: msg.conversationId,
|
|
657
|
+
catalogKey: chatCatalogKey(msg.conversationId),
|
|
658
|
+
requestId: msg.requestId || null,
|
|
659
|
+
mode,
|
|
660
|
+
cursor: msg.beforeId ?? msg.afterMessageId ?? null,
|
|
661
|
+
afterMessageId: msg.afterMessageId ?? null,
|
|
492
662
|
messages,
|
|
493
663
|
hasMore,
|
|
494
664
|
total
|
|
@@ -509,7 +679,7 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
509
679
|
|
|
510
680
|
// Ownership check when explicit conversationId is provided
|
|
511
681
|
if (msg.conversationId && !CONFIG.skipAuth) {
|
|
512
|
-
if (!verifyConversationOwnership(msg.conversationId, client.userId)) {
|
|
682
|
+
if (!verifyConversationOwnership(msg.conversationId, client.userId, client.role)) {
|
|
513
683
|
await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
|
|
514
684
|
return;
|
|
515
685
|
}
|
|
@@ -684,7 +854,7 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
684
854
|
if (!client.currentAgent) return;
|
|
685
855
|
if (!await checkAgentAccess(client.currentAgent)) return;
|
|
686
856
|
const cancelConvId = msg.conversationId || client.currentConversation;
|
|
687
|
-
if (!CONFIG.skipAuth && !verifyConversationOwnership(cancelConvId, client.userId)) {
|
|
857
|
+
if (!CONFIG.skipAuth && !verifyConversationOwnership(cancelConvId, client.userId, client.role)) {
|
|
688
858
|
console.warn(`[Security] User ${client.userId} cancel denied for ${cancelConvId}`);
|
|
689
859
|
await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
|
|
690
860
|
return;
|
|
@@ -701,7 +871,7 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
701
871
|
if (!refreshAgent) return;
|
|
702
872
|
if (!await checkAgentAccess(refreshAgent)) return;
|
|
703
873
|
const refreshConvId = msg.conversationId || client.currentConversation;
|
|
704
|
-
if (!CONFIG.skipAuth && !verifyConversationOwnership(refreshConvId, client.userId)) {
|
|
874
|
+
if (!CONFIG.skipAuth && !verifyConversationOwnership(refreshConvId, client.userId, client.role)) {
|
|
705
875
|
console.warn(`[Security] User ${client.userId} refresh denied for ${refreshConvId}`);
|
|
706
876
|
await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
|
|
707
877
|
return;
|
|
@@ -746,11 +916,14 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
746
916
|
}
|
|
747
917
|
|
|
748
918
|
case 'update_conversation_settings': {
|
|
749
|
-
if (!client.currentAgent) return;
|
|
750
|
-
if (!await checkAgentAccess(client.currentAgent)) return;
|
|
751
919
|
const settingsConvId = msg.conversationId || client.currentConversation;
|
|
752
|
-
|
|
753
|
-
|
|
920
|
+
const settingsRow = settingsConvId ? sessionDb.get(settingsConvId) : null;
|
|
921
|
+
const settingsAgentId = msg.agentId || settingsRow?.agent_id || client.currentAgent;
|
|
922
|
+
if (!settingsConvId || !settingsAgentId) return;
|
|
923
|
+
if (msg.disallowedTools !== undefined && !await checkAgentAccess(settingsAgentId)) return;
|
|
924
|
+
if (!CONFIG.skipAuth && (!verifyConversationOwnership(settingsConvId, client.userId, client.role)
|
|
925
|
+
|| (agents.has(settingsAgentId) && !verifyAgentOwnership(settingsAgentId, client.userId, client.role))
|
|
926
|
+
|| (settingsRow?.agent_id && settingsRow.agent_id !== settingsAgentId))) {
|
|
754
927
|
console.warn(`[Security] User ${client.userId} settings update denied for ${settingsConvId}`);
|
|
755
928
|
await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
|
|
756
929
|
return;
|
|
@@ -762,7 +935,7 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
762
935
|
// line 351 silently clobbers the user's renamed title the next
|
|
763
936
|
// time `convInfo.customTitle` is reset to undefined.
|
|
764
937
|
if (msg.title !== undefined) {
|
|
765
|
-
const titleAgent = agents.get(
|
|
938
|
+
const titleAgent = agents.get(settingsAgentId);
|
|
766
939
|
const titleConvInfo = titleAgent?.conversations.get(settingsConvId);
|
|
767
940
|
if (msg.title) {
|
|
768
941
|
sessionDb.update(settingsConvId, { title: msg.title, isCustomTitle: 1 });
|
|
@@ -776,12 +949,13 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
776
949
|
}
|
|
777
950
|
// Only forward to agent if disallowedTools present
|
|
778
951
|
if (msg.disallowedTools) {
|
|
779
|
-
await forwardToAgent(
|
|
952
|
+
await forwardToAgent(settingsAgentId, {
|
|
780
953
|
type: 'update_conversation_settings',
|
|
781
954
|
conversationId: settingsConvId,
|
|
782
955
|
disallowedTools: msg.disallowedTools
|
|
783
956
|
});
|
|
784
957
|
}
|
|
958
|
+
if (msg.title !== undefined) await broadcastSessionCatalog(client.userId);
|
|
785
959
|
break;
|
|
786
960
|
}
|
|
787
961
|
|
|
@@ -790,7 +964,7 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
790
964
|
if (!await checkAgentAccess(client.currentAgent)) return;
|
|
791
965
|
const answerConvId = msg.conversationId || client.currentConversation;
|
|
792
966
|
if (!answerConvId) return;
|
|
793
|
-
if (!CONFIG.skipAuth && !verifyConversationOwnership(answerConvId, client.userId)) {
|
|
967
|
+
if (!CONFIG.skipAuth && !verifyConversationOwnership(answerConvId, client.userId, client.role)) {
|
|
794
968
|
console.warn(`[Security] User ${client.userId} ask_user_answer denied for ${answerConvId}`);
|
|
795
969
|
return;
|
|
796
970
|
}
|
|
@@ -841,7 +1015,7 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
841
1015
|
await sendToWebClient(client, { type: 'btw_error', error: 'No conversation selected' });
|
|
842
1016
|
return;
|
|
843
1017
|
}
|
|
844
|
-
if (!CONFIG.skipAuth && !verifyConversationOwnership(btwConvId, client.userId)) {
|
|
1018
|
+
if (!CONFIG.skipAuth && !verifyConversationOwnership(btwConvId, client.userId, client.role)) {
|
|
845
1019
|
console.warn(`[Security] User ${client.userId} btw_question denied for ${btwConvId}`);
|
|
846
1020
|
await sendToWebClient(client, { type: 'btw_error', conversationId: btwConvId, error: 'Permission denied' });
|
|
847
1021
|
return;
|
|
@@ -37,7 +37,7 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
|
|
|
37
37
|
if (!await checkAgentAccess(termAgentId)) return;
|
|
38
38
|
const termConvId = msg.conversationId || client.currentConversation;
|
|
39
39
|
if (!termConvId) return;
|
|
40
|
-
if (!CONFIG.skipAuth && !isYeaftVirtualConversation(termConvId) && !verifyConversationOwnership(termConvId, client.userId)) {
|
|
40
|
+
if (!CONFIG.skipAuth && !isYeaftVirtualConversation(termConvId) && !verifyConversationOwnership(termConvId, client.userId, client.role)) {
|
|
41
41
|
console.warn(`[Security] User ${client.userId} terminal access denied for ${termConvId}`);
|
|
42
42
|
await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
|
|
43
43
|
return;
|
|
@@ -68,7 +68,7 @@ export async function handleClientWorkbench(clientId, client, msg, checkAgentAcc
|
|
|
68
68
|
const writeConvId = msg.conversationId || client.currentConversation || '_explorer';
|
|
69
69
|
const isAgentLevelWrite = writeConvId.startsWith('_') || isYeaftVirtualConversation(writeConvId);
|
|
70
70
|
if (!isAgentLevelWrite) {
|
|
71
|
-
if (!CONFIG.skipAuth && !verifyConversationOwnership(writeConvId, client.userId)) {
|
|
71
|
+
if (!CONFIG.skipAuth && !verifyConversationOwnership(writeConvId, client.userId, client.role)) {
|
|
72
72
|
console.warn(`[Security] User ${client.userId} file write denied for ${writeConvId}`);
|
|
73
73
|
await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
|
|
74
74
|
return;
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* @typedef {Object} PinRouteDeps
|
|
27
|
-
* @property {(id: string) =>
|
|
27
|
+
* @property {(id: string) => Array<{ userId?: string|null, agentId?: string|null }>} getYeaftRows Reads all matching Yeaft Session rows.
|
|
28
28
|
* @property {(id: string, userId: string) => boolean} verifyChatOwnership Returns true if `userId` owns the chat conversation `id`.
|
|
29
29
|
* @property {boolean} [skipAuth] Skip both ownership checks (dev / single-user).
|
|
30
30
|
*/
|
|
@@ -47,12 +47,13 @@ export function routeSessionPin(deps, client, msg) {
|
|
|
47
47
|
const id = msg && msg.conversationId;
|
|
48
48
|
if (!id) return { kind: 'noop' };
|
|
49
49
|
const isPinned = msg.type === 'pin_session';
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
const yeaftRows = deps.getYeaftRows(id);
|
|
51
|
+
if (yeaftRows.length > 0) {
|
|
52
|
+
const owned = yeaftRows.filter(row => deps.skipAuth || row.userId === client.userId);
|
|
53
|
+
if (owned.length !== 1 || yeaftRows.length !== 1) {
|
|
54
|
+
return { kind: 'denied', id, reason: owned.length === 0 ? 'yeaft-foreign' : 'yeaft-ambiguous' };
|
|
54
55
|
}
|
|
55
|
-
return { kind: 'yeaft', id, isPinned };
|
|
56
|
+
return { kind: 'yeaft', id, agentId: owned[0].agentId, isPinned };
|
|
56
57
|
}
|
|
57
58
|
if (!deps.skipAuth && !deps.verifyChatOwnership(id, client.userId)) {
|
|
58
59
|
return { kind: 'denied', id, reason: 'chat-foreign' };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const CHAT_RUNTIME_PROVIDERS = new Set(['claude-code', 'copilot']);
|
|
2
|
+
|
|
3
|
+
export function normalizeChatRuntimeProvider(provider) {
|
|
4
|
+
if (provider == null || provider === '') return 'claude-code';
|
|
5
|
+
if (CHAT_RUNTIME_PROVIDERS.has(provider)) return provider;
|
|
6
|
+
throw new Error(`Unknown Chat runtime provider: ${provider}`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function chatCatalogKey(conversationId) {
|
|
10
|
+
if (typeof conversationId !== 'string' || !conversationId) {
|
|
11
|
+
throw new Error('Chat catalog key requires conversationId');
|
|
12
|
+
}
|
|
13
|
+
return `chat:${conversationId}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function yeaftCatalogKey(agentId, sessionId) {
|
|
17
|
+
if (typeof agentId !== 'string' || !agentId || typeof sessionId !== 'string' || !sessionId) {
|
|
18
|
+
throw new Error('Yeaft catalog key requires agentId and sessionId');
|
|
19
|
+
}
|
|
20
|
+
return `yeaft:${agentId}:${sessionId}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function timestampValue(value) {
|
|
24
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
|
|
25
|
+
if (typeof value !== 'string' || !value) return 0;
|
|
26
|
+
const numeric = Number(value);
|
|
27
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
28
|
+
const parsed = Date.parse(value);
|
|
29
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function projectSessionCatalog({
|
|
33
|
+
chatSessions = [],
|
|
34
|
+
yeaftSessions = [],
|
|
35
|
+
metadata = [],
|
|
36
|
+
onlineAgentIds = new Set(),
|
|
37
|
+
} = {}) {
|
|
38
|
+
const metadataByKey = new Map(metadata.map(row => [row.catalogKey, row]));
|
|
39
|
+
const onlineAgents = onlineAgentIds instanceof Set ? onlineAgentIds : new Set(onlineAgentIds);
|
|
40
|
+
const rows = [];
|
|
41
|
+
|
|
42
|
+
for (const session of chatSessions) {
|
|
43
|
+
if (session.is_active === 0) continue;
|
|
44
|
+
const catalogKey = chatCatalogKey(session.id);
|
|
45
|
+
const runtimeProvider = normalizeChatRuntimeProvider(session.provider);
|
|
46
|
+
const meta = metadataByKey.get(catalogKey) || {};
|
|
47
|
+
rows.push({
|
|
48
|
+
catalogKey,
|
|
49
|
+
runtimeProvider,
|
|
50
|
+
routeRef: { runtimeProvider, agentId: session.agent_id, sessionId: session.id },
|
|
51
|
+
title: session.title || session.id,
|
|
52
|
+
workDir: session.work_dir || '',
|
|
53
|
+
agentId: session.agent_id,
|
|
54
|
+
agentName: session.agent_name || '',
|
|
55
|
+
availability: onlineAgents.has(session.agent_id) ? 'online' : 'offline',
|
|
56
|
+
pinned: meta.pinned ?? session.is_pinned === 1,
|
|
57
|
+
sortRank: meta.sortRank ?? null,
|
|
58
|
+
createdAt: session.created_at || null,
|
|
59
|
+
updatedAt: session.updated_at || null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const session of yeaftSessions) {
|
|
64
|
+
const catalogKey = yeaftCatalogKey(session.agentId, session.id);
|
|
65
|
+
const meta = metadataByKey.get(catalogKey) || {};
|
|
66
|
+
rows.push({
|
|
67
|
+
catalogKey,
|
|
68
|
+
runtimeProvider: 'yeaft',
|
|
69
|
+
routeRef: { runtimeProvider: 'yeaft', agentId: session.agentId, sessionId: session.id },
|
|
70
|
+
title: session.name || session.id,
|
|
71
|
+
workDir: session.workDir || '',
|
|
72
|
+
agentId: session.agentId,
|
|
73
|
+
agentName: session.agentName || '',
|
|
74
|
+
availability: onlineAgents.has(session.agentId) ? 'online' : 'offline',
|
|
75
|
+
pinned: meta.pinned ?? !!session.pinned,
|
|
76
|
+
sortRank: meta.sortRank ?? session.sortOrder ?? null,
|
|
77
|
+
createdAt: session.createdAt || null,
|
|
78
|
+
updatedAt: session.updatedAt || null,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return rows.sort((left, right) => {
|
|
83
|
+
if (left.pinned !== right.pinned) return left.pinned ? -1 : 1;
|
|
84
|
+
const leftRank = Number.isFinite(left.sortRank) ? left.sortRank : Number.MAX_SAFE_INTEGER;
|
|
85
|
+
const rightRank = Number.isFinite(right.sortRank) ? right.sortRank : Number.MAX_SAFE_INTEGER;
|
|
86
|
+
if (leftRank !== rightRank) return leftRank - rightRank;
|
|
87
|
+
const creationDelta = timestampValue(right.createdAt) - timestampValue(left.createdAt);
|
|
88
|
+
if (creationDelta !== 0) return creationDelta;
|
|
89
|
+
return left.catalogKey.localeCompare(right.catalogKey);
|
|
90
|
+
});
|
|
91
|
+
}
|