@yeaft/webchat-agent 1.0.352 → 1.0.353
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/local-runtime/server/context.js +1 -1
- package/local-runtime/server/handlers/client-misc.js +32 -0
- package/local-runtime/server/ws-agent.js +55 -53
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +32 -32
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
|
@@ -16,7 +16,7 @@ export const pendingProxyRequests = new Map(); // requestId → { res, timeout,
|
|
|
16
16
|
export const proxyWsConnections = new Map(); // proxyWsId → { browserWs, agentId }
|
|
17
17
|
|
|
18
18
|
// Store pending agent connections (waiting for auth message)
|
|
19
|
-
// tempId -> { ws, agentId, agentName, workDir, timeout }
|
|
19
|
+
// tempId -> { ws, agentId, agentName, instanceId, workDir, skipAgentAuth, connectionGeneration, timeout }
|
|
20
20
|
export const pendingAgentConnections = new Map();
|
|
21
21
|
|
|
22
22
|
// ★ Phase 3: Server-side message queues
|
|
@@ -3,6 +3,25 @@ import {
|
|
|
3
3
|
sendToWebClient, forwardToAgent, broadcastAgentList
|
|
4
4
|
} from '../ws-utils.js';
|
|
5
5
|
|
|
6
|
+
// v1.0.342 is the first release with the bootstrap -> detached runner handoff.
|
|
7
|
+
// Older Windows Agents can lose their updater to PM2 tree-kill before npm runs.
|
|
8
|
+
export const MIN_SAFE_REMOTE_UPGRADE_VERSION = '1.0.342';
|
|
9
|
+
|
|
10
|
+
function parseVersion(version) {
|
|
11
|
+
const match = String(version || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)$/u);
|
|
12
|
+
return match ? match.slice(1).map(Number) : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function requiresManualUpgradeBridge(version) {
|
|
16
|
+
const current = parseVersion(version);
|
|
17
|
+
const minimum = parseVersion(MIN_SAFE_REMOTE_UPGRADE_VERSION);
|
|
18
|
+
if (!current || !minimum) return true;
|
|
19
|
+
for (let index = 0; index < minimum.length; index++) {
|
|
20
|
+
if (current[index] !== minimum[index]) return current[index] < minimum[index];
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
/**
|
|
7
26
|
* Handle miscellaneous messages from web client.
|
|
8
27
|
* Types: ping, restart_agent, upgrade_agent,
|
|
@@ -26,6 +45,19 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
|
|
|
26
45
|
const upgradeAgentId = msg.agentId;
|
|
27
46
|
if (!upgradeAgentId) break;
|
|
28
47
|
if (!await checkAgentAccess(upgradeAgentId)) break;
|
|
48
|
+
const upgradeAgent = agents.get(upgradeAgentId);
|
|
49
|
+
if (requiresManualUpgradeBridge(upgradeAgent?.version)) {
|
|
50
|
+
await sendToWebClient(client, {
|
|
51
|
+
type: 'upgrade_agent_ack',
|
|
52
|
+
agentId: upgradeAgentId,
|
|
53
|
+
success: false,
|
|
54
|
+
reason: 'manual_upgrade_required',
|
|
55
|
+
version: upgradeAgent?.version || null,
|
|
56
|
+
minimumVersion: MIN_SAFE_REMOTE_UPGRADE_VERSION,
|
|
57
|
+
error: `Agent ${upgradeAgent?.version || 'unknown'} predates the safe remote-upgrade handoff; manually install ${MIN_SAFE_REMOTE_UPGRADE_VERSION} or newer once`,
|
|
58
|
+
});
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
29
61
|
await forwardToAgent(upgradeAgentId, { type: 'upgrade_agent' });
|
|
30
62
|
break;
|
|
31
63
|
}
|
|
@@ -25,60 +25,45 @@ function buildAgentMapKey(ownerId, agentName) {
|
|
|
25
25
|
return `${prefix}:${agentName}`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
let nextAgentConnectionGeneration = 0;
|
|
29
|
+
// Keep the latest generation as a tombstone while an older auth may still arrive.
|
|
30
|
+
const latestAgentConnectionGenerations = new Map();
|
|
31
|
+
|
|
32
|
+
function claimAgentConnection(agentId, generation) {
|
|
33
|
+
const latestGeneration = latestAgentConnectionGenerations.get(agentId);
|
|
34
|
+
if (latestGeneration !== undefined && latestGeneration > generation) return false;
|
|
35
|
+
latestAgentConnectionGenerations.set(agentId, generation);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pruneAgentConnectionGenerations() {
|
|
40
|
+
for (const [agentId, generation] of latestAgentConnectionGenerations) {
|
|
41
|
+
if (agents.has(agentId)) continue;
|
|
42
|
+
const hasPotentiallyOlderConnection = [...pendingAgentConnections.values()].some(pending => {
|
|
43
|
+
if (pending.connectionGeneration >= generation) return false;
|
|
44
|
+
return pending.skipAgentAuth ? pending.agentId === agentId : true;
|
|
45
|
+
});
|
|
46
|
+
if (!hasPotentiallyOlderConnection) latestAgentConnectionGenerations.delete(agentId);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
export function handleAgentConnection(ws, url) {
|
|
29
51
|
const clientAgentId = url.searchParams.get('id') || randomUUID();
|
|
30
52
|
const agentName = url.searchParams.get('name') || `Agent-${clientAgentId.slice(0, 8)}`;
|
|
31
53
|
const instanceId = url.searchParams.get('instanceId') || clientAgentId;
|
|
32
54
|
const workDir = url.searchParams.get('workDir') || '';
|
|
33
55
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
// Both authenticated and SKIP_AUTH connections use the existing auth frame
|
|
57
|
+
// for registration metadata. Released Agents already send their version there;
|
|
58
|
+
// SKIP_AUTH only bypasses secret validation and owner scoping.
|
|
59
|
+
const skipAgentAuth = CONFIG.skipAuth;
|
|
60
|
+
const urlCapabilities = (url.searchParams.get('capabilities') || '').split(',').filter(Boolean);
|
|
61
|
+
const connectionGeneration = ++nextAgentConnectionGeneration;
|
|
39
62
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (!agent) console.error(`[Agent] No agent found for id: ${clientAgentId}`);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
markAgentHeartbeatSeen(agent);
|
|
47
|
-
const msg = await parseMessage(data, agent.sessionKey);
|
|
48
|
-
if (msg) {
|
|
49
|
-
console.log(`[Agent] Received message from ${clientAgentId}: ${msg.type}`);
|
|
50
|
-
if (msg.perfTraceId) {
|
|
51
|
-
recordPerfTraceEvent({
|
|
52
|
-
traceId: msg.perfTraceId,
|
|
53
|
-
source: 'server',
|
|
54
|
-
phase: 'websocket.agent_received',
|
|
55
|
-
at: Date.now(),
|
|
56
|
-
agentId: clientAgentId,
|
|
57
|
-
sessionId: msg.sessionId || null,
|
|
58
|
-
vpId: msg.vpId || null,
|
|
59
|
-
turnId: msg.turnId || null,
|
|
60
|
-
threadId: msg.threadId || null,
|
|
61
|
-
messageType: msg.type,
|
|
62
|
-
bytes: data.length || 0,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
handleAgentMessage(clientAgentId, msg, ws);
|
|
66
|
-
} else {
|
|
67
|
-
console.error(`[Agent] Failed to parse message from ${clientAgentId}`);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
63
|
+
// SKIP_AUTH has a complete identity at connect time. Authenticated connections
|
|
64
|
+
// can only claim an owner-scoped identity after their secret is verified.
|
|
65
|
+
if (skipAgentAuth) claimAgentConnection(clientAgentId, connectionGeneration);
|
|
70
66
|
|
|
71
|
-
ws.on('close', () => {
|
|
72
|
-
handleAgentDisconnect(clientAgentId, agentName, ws);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
ws.on('error', (err) => {
|
|
76
|
-
console.error(`Agent error (${agentName}):`, err.message);
|
|
77
|
-
});
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// In production mode, wait for auth message with secret
|
|
82
67
|
const tempId = randomUUID();
|
|
83
68
|
// Mutable: will be updated to the owner-scoped key after auth succeeds
|
|
84
69
|
let resolvedAgentId = null;
|
|
@@ -86,6 +71,7 @@ export function handleAgentConnection(ws, url) {
|
|
|
86
71
|
const authTimeout = setTimeout(() => {
|
|
87
72
|
console.log(`Agent auth timeout: ${agentName}`);
|
|
88
73
|
pendingAgentConnections.delete(tempId);
|
|
74
|
+
pruneAgentConnectionGenerations();
|
|
89
75
|
ws.close(1008, 'Authentication timeout');
|
|
90
76
|
}, 30000);
|
|
91
77
|
|
|
@@ -95,10 +81,12 @@ export function handleAgentConnection(ws, url) {
|
|
|
95
81
|
agentName,
|
|
96
82
|
instanceId,
|
|
97
83
|
workDir,
|
|
84
|
+
skipAgentAuth,
|
|
85
|
+
connectionGeneration,
|
|
98
86
|
timeout: authTimeout
|
|
99
87
|
});
|
|
100
88
|
|
|
101
|
-
// Request
|
|
89
|
+
// Request the existing registration frame. SKIP_AUTH ignores its secret.
|
|
102
90
|
ws.send(JSON.stringify({
|
|
103
91
|
type: 'auth_required',
|
|
104
92
|
tempId
|
|
@@ -114,22 +102,34 @@ export function handleAgentConnection(ws, url) {
|
|
|
114
102
|
clearTimeout(pending.timeout);
|
|
115
103
|
pendingAgentConnections.delete(tempId);
|
|
116
104
|
|
|
117
|
-
const authResult =
|
|
105
|
+
const authResult = skipAgentAuth
|
|
106
|
+
? { valid: true, sessionKey: null, userId: null, username: null }
|
|
107
|
+
: verifyAgent(msg.secret);
|
|
118
108
|
if (!authResult.valid) {
|
|
109
|
+
pruneAgentConnectionGenerations();
|
|
119
110
|
console.log(`Agent auth failed: ${agentName}`);
|
|
120
111
|
ws.close(1008, 'Invalid agent secret');
|
|
121
112
|
return;
|
|
122
113
|
}
|
|
123
114
|
|
|
124
|
-
const capabilities = msg.capabilities
|
|
115
|
+
const capabilities = Array.isArray(msg.capabilities) ? msg.capabilities : urlCapabilities;
|
|
125
116
|
const agentVersion = msg.version || null;
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
117
|
+
// Authenticated Agents use an owner-scoped key. SKIP_AUTH preserves
|
|
118
|
+
// its historical unscoped id while still receiving version metadata.
|
|
119
|
+
resolvedAgentId = skipAgentAuth
|
|
120
|
+
? clientAgentId
|
|
121
|
+
: buildAgentMapKey(authResult.userId, pending.instanceId || pending.agentId || pending.agentName);
|
|
122
|
+
if (!claimAgentConnection(resolvedAgentId, connectionGeneration)) {
|
|
123
|
+
resolvedAgentId = null;
|
|
124
|
+
pruneAgentConnectionGenerations();
|
|
125
|
+
ws.close(1008, 'Superseded by a newer Agent connection');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
130
128
|
completeAgentRegistration(ws, resolvedAgentId, pending.agentName, pending.workDir, authResult.sessionKey, capabilities, authResult.userId, authResult.username, agentVersion, pending.instanceId || pending.agentId || pending.agentName);
|
|
129
|
+
pruneAgentConnectionGenerations();
|
|
131
130
|
}
|
|
132
131
|
} catch (e) {
|
|
132
|
+
pruneAgentConnectionGenerations();
|
|
133
133
|
console.error('Failed to parse agent auth message:', e.message);
|
|
134
134
|
}
|
|
135
135
|
} else {
|
|
@@ -171,6 +171,7 @@ export function handleAgentConnection(ws, url) {
|
|
|
171
171
|
if (pending) {
|
|
172
172
|
clearTimeout(pending.timeout);
|
|
173
173
|
pendingAgentConnections.delete(tempId);
|
|
174
|
+
pruneAgentConnectionGenerations();
|
|
174
175
|
}
|
|
175
176
|
// Use resolvedAgentId if auth completed, otherwise nothing to clean
|
|
176
177
|
if (resolvedAgentId) {
|
|
@@ -199,6 +200,7 @@ function handleAgentDisconnect(agentId, agentName, ws) {
|
|
|
199
200
|
}
|
|
200
201
|
// Remove agent entirely — eliminates zombie agents from broadcastAgentList
|
|
201
202
|
agents.delete(agentId);
|
|
203
|
+
pruneAgentConnectionGenerations();
|
|
202
204
|
console.log(`Agent disconnected: ${agentName}`);
|
|
203
205
|
broadcastAgentList();
|
|
204
206
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.353"}
|