@yeaft/webchat-agent 1.0.416 → 1.0.418
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 +39 -0
- package/browser-runtime/chromium.js +192 -0
- package/browser-runtime/cli.js +1 -1
- package/browser-runtime/extension/manifest.json +3 -0
- package/browser-runtime/extension/offscreen.js +201 -25
- package/browser-runtime/extension/popup.js +4 -1
- package/browser-runtime/extension/service-worker.js +47 -7
- package/browser-runtime/extension.js +1 -1
- package/browser-runtime/index.js +3 -0
- package/browser-runtime/local-bridge.js +194 -0
- package/browser-runtime/messages.js +123 -0
- package/browser-runtime/service.js +769 -30
- package/connection/index.js +2 -0
- package/connection/message-router.js +2 -0
- package/index.js +13 -0
- package/local-runtime/server/.env.example +10 -0
- package/local-runtime/server/browser-runtime-routes.js +284 -0
- package/local-runtime/server/client-protocol.js +8 -0
- package/local-runtime/server/config.js +28 -0
- package/local-runtime/server/handlers/agent-browser.js +393 -0
- package/local-runtime/server/handlers/agent-sync.js +14 -0
- package/local-runtime/server/handlers/client-browser.js +337 -0
- package/local-runtime/server/ws-agent.js +9 -1
- package/local-runtime/server/ws-client.js +41 -2
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +220 -98
- 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
package/connection/index.js
CHANGED
|
@@ -53,6 +53,7 @@ export function connect(WebSocketImpl = WebSocket) {
|
|
|
53
53
|
if (closedTerminals > 0) {
|
|
54
54
|
console.log(`[PTY] Closed ${closedTerminals} terminal(s) before Agent transport replacement`);
|
|
55
55
|
}
|
|
56
|
+
void ctx.browserRuntime?.handleTransportDisconnect?.();
|
|
56
57
|
}
|
|
57
58
|
ctx.ws = socket;
|
|
58
59
|
|
|
@@ -111,6 +112,7 @@ export function connect(WebSocketImpl = WebSocket) {
|
|
|
111
112
|
if (closedTerminals > 0) {
|
|
112
113
|
console.log(`[PTY] Closed ${closedTerminals} terminal(s) after Agent transport disconnect`);
|
|
113
114
|
}
|
|
115
|
+
void ctx.browserRuntime?.handleTransportDisconnect?.();
|
|
114
116
|
|
|
115
117
|
if (code === 1008) {
|
|
116
118
|
console.error('Authentication failed. Check AGENT_SECRET configuration.');
|
|
@@ -33,6 +33,7 @@ import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
|
|
|
33
33
|
import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, refreshLiveSessionConfig, handleYeaftLoadHistory, handleYeaftLoadHistoryOutline, handleYeaftSearchHistory, handleYeaftLoadHistoryWindow, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftProjectContextSync, handleYeaftProjectMutation, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, handleYeaftPluginCatalog, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, broadcastYeaftVpSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
|
|
34
34
|
import { startYeaftStatusRefresh, forceRefreshYeaftStatus } from '../yeaft/status-cache.js';
|
|
35
35
|
import { handleWorkCenterRequest } from '../yeaft/work-center/bridge.js';
|
|
36
|
+
import { handleBrowserRuntimeMessage } from '../browser-runtime/messages.js';
|
|
36
37
|
|
|
37
38
|
export async function applyLlmConfigUpdate(msg, dependencies = {}) {
|
|
38
39
|
const updateConfig = dependencies.updateLlmConfig || updateLlmConfig;
|
|
@@ -94,6 +95,7 @@ export function applyRegisteredTransport(msg) {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
export async function handleMessage(msg) {
|
|
98
|
+
if (await handleBrowserRuntimeMessage(msg)) return;
|
|
97
99
|
switch (msg.type) {
|
|
98
100
|
case 'registered':
|
|
99
101
|
applyRegisteredTransport(msg);
|
package/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { connect } from './connection.js';
|
|
|
23
23
|
import { loadMcpServers } from './mcp.js';
|
|
24
24
|
import { SAFE_REMOTE_UPGRADE_CAPABILITY } from './upgrade-command.js';
|
|
25
25
|
import { loadConfig as loadYeaftConfig } from './yeaft/config.js';
|
|
26
|
+
import { updateBrowserRuntimeSettings } from './yeaft/config-api.js';
|
|
26
27
|
import { bootBrowserRuntime, shutdownBrowserRuntime } from './browser-runtime/index.js';
|
|
27
28
|
import {
|
|
28
29
|
ensureManagedCliTools,
|
|
@@ -164,6 +165,9 @@ async function detectCapabilities() {
|
|
|
164
165
|
if (process.platform === 'linux') capabilities.push('work_item_attachments');
|
|
165
166
|
const pty = await loadNodePty();
|
|
166
167
|
if (pty) capabilities.push('terminal');
|
|
168
|
+
if (ctx.browserRuntime) {
|
|
169
|
+
capabilities.push(...ctx.browserRuntime.setupCapabilities(), ...ctx.browserRuntime.capabilities());
|
|
170
|
+
}
|
|
167
171
|
|
|
168
172
|
console.log(`[Capabilities] Detected: ${capabilities.join(', ')}`);
|
|
169
173
|
return capabilities;
|
|
@@ -403,6 +407,15 @@ process.on('SIGTERM', async () => {
|
|
|
403
407
|
ctx.browserRuntime = await bootBrowserRuntime({
|
|
404
408
|
yeaftDir: YEAFT_DIR,
|
|
405
409
|
config: runtimeConfig,
|
|
410
|
+
saveSettings: update => updateBrowserRuntimeSettings(update, YEAFT_DIR),
|
|
411
|
+
onCapabilitiesChanged: async () => {
|
|
412
|
+
ctx.agentCapabilities = await detectCapabilities();
|
|
413
|
+
ctx.sendToServer?.({
|
|
414
|
+
type: 'agent_capabilities_updated',
|
|
415
|
+
capabilities: ctx.agentCapabilities,
|
|
416
|
+
});
|
|
417
|
+
},
|
|
418
|
+
send: message => ctx.sendToServer?.(message) || 'dropped',
|
|
406
419
|
});
|
|
407
420
|
const probe = ctx.browserRuntime.snapshot().probe;
|
|
408
421
|
if (probe?.ok) {
|
|
@@ -44,6 +44,16 @@ EMAIL_CODE_EXPIRES_IN=300000
|
|
|
44
44
|
# Agents must provide this secret to connect
|
|
45
45
|
AGENT_SECRET=agent-shared-secret
|
|
46
46
|
|
|
47
|
+
# Browser Runtime (disabled by default)
|
|
48
|
+
# Production deployments should configure endpoint-scoped TURN credentials.
|
|
49
|
+
# BROWSER_RUNTIME_ENABLED=false
|
|
50
|
+
# BROWSER_STUN_URLS=stun:stun.example.com:3478
|
|
51
|
+
# BROWSER_TURN_URLS=turn:turn.example.com:3478,turns:turn.example.com:443?transport=tcp
|
|
52
|
+
# BROWSER_TURN_SECRET=replace-with-coturn-rest-api-secret
|
|
53
|
+
# BROWSER_TURN_TTL_SECONDS=600
|
|
54
|
+
# BROWSER_ICE_TRANSPORT_POLICY=all
|
|
55
|
+
# BROWSER_ROUTE_TTL_MS=900000
|
|
56
|
+
|
|
47
57
|
# Server-managed Sandbox (disabled by default)
|
|
48
58
|
# Docker socket access is equivalent to Host root. For Docker Compose, use the
|
|
49
59
|
# explicit docker-compose.sandbox.yml override documented in docs/operations/sandbox-agent.md.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { createHmac, randomUUID } from 'node:crypto';
|
|
2
|
+
import { CONFIG } from './config.js';
|
|
3
|
+
import { agents, webClients } from './context.js';
|
|
4
|
+
|
|
5
|
+
const MAX_ROUTES = 2048;
|
|
6
|
+
const MAX_PEERS = 4096;
|
|
7
|
+
const MAX_CREATE_REQUESTS = 4096;
|
|
8
|
+
const CREATE_REQUEST_TTL_MS = 10 * 60_000;
|
|
9
|
+
const INSTALL_REQUEST_TTL_MS = 60 * 60_000;
|
|
10
|
+
|
|
11
|
+
export const browserRoutes = new Map();
|
|
12
|
+
export const browserPeers = new Map();
|
|
13
|
+
const browserRequests = new Map();
|
|
14
|
+
|
|
15
|
+
function key(agentId, browserSessionId) {
|
|
16
|
+
return `${String(agentId || '')}\0${String(browserSessionId || '')}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createKey(agentId, connectionId, requestId) {
|
|
20
|
+
return `${String(agentId || '')}\0${String(connectionId || '')}\0${String(requestId || '')}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function scopeUsername({ ownerUserId, agentId, browserSessionId, peerId, connectionGeneration, endpointRole, expiresAt }) {
|
|
24
|
+
const scope = Buffer.from(JSON.stringify({
|
|
25
|
+
ownerUserId,
|
|
26
|
+
agentId,
|
|
27
|
+
browserSessionId,
|
|
28
|
+
peerId,
|
|
29
|
+
connectionGeneration,
|
|
30
|
+
endpointRole,
|
|
31
|
+
credentialId: randomUUID(),
|
|
32
|
+
})).toString('base64url');
|
|
33
|
+
return `${Math.floor(expiresAt / 1000)}:${scope}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function mintBrowserIceServers(scope, config = CONFIG.browserRuntime) {
|
|
37
|
+
const stunServers = config.stunUrls.length > 0 ? [{ urls: [...config.stunUrls] }] : [];
|
|
38
|
+
if (config.turnUrls.length === 0) return stunServers;
|
|
39
|
+
const expiresAt = Date.now() + config.credentialTtlSeconds * 1000;
|
|
40
|
+
const username = scopeUsername({ ...scope, expiresAt });
|
|
41
|
+
const credential = createHmac('sha1', config.turnSecret).update(username).digest('base64');
|
|
42
|
+
return [
|
|
43
|
+
...stunServers,
|
|
44
|
+
{ urls: [...config.turnUrls], username, credential, expiresAt },
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function browserServerIdentity(client) {
|
|
49
|
+
return Object.freeze({
|
|
50
|
+
ownerUserId: client.userId,
|
|
51
|
+
clientId: client.id,
|
|
52
|
+
webConnectionId: client.connectionId,
|
|
53
|
+
webConnectionGeneration: client.connectionGeneration,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function pruneBrowserRuntimeRoutes(now = Date.now()) {
|
|
58
|
+
for (const [requestKey, request] of browserRequests) {
|
|
59
|
+
if (request.expiresAt <= now || !webClients.has(request.clientId)) browserRequests.delete(requestKey);
|
|
60
|
+
}
|
|
61
|
+
for (const [peerId, peer] of browserPeers) {
|
|
62
|
+
const client = webClients.get(peer.clientId);
|
|
63
|
+
if ((peer.expiresAt != null && peer.expiresAt <= now)
|
|
64
|
+
|| !client || client.connectionId !== peer.webConnectionId) {
|
|
65
|
+
browserPeers.delete(peerId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function registerBrowserCreateRequest({
|
|
71
|
+
agentId,
|
|
72
|
+
client,
|
|
73
|
+
requestId,
|
|
74
|
+
digest,
|
|
75
|
+
kind = 'browser_session_create',
|
|
76
|
+
}) {
|
|
77
|
+
pruneBrowserRuntimeRoutes();
|
|
78
|
+
const requestKey = createKey(agentId, client.connectionId, requestId);
|
|
79
|
+
const existing = browserRequests.get(requestKey);
|
|
80
|
+
if (existing) {
|
|
81
|
+
if (existing.digest !== digest) return { conflict: true, request: existing };
|
|
82
|
+
return { duplicate: true, request: existing };
|
|
83
|
+
}
|
|
84
|
+
if (browserRequests.size >= MAX_CREATE_REQUESTS) return { capacity: true, request: null };
|
|
85
|
+
const request = {
|
|
86
|
+
agentId,
|
|
87
|
+
requestId,
|
|
88
|
+
serverRequestId: randomUUID(),
|
|
89
|
+
clientId: client.id,
|
|
90
|
+
ownerUserId: client.userId,
|
|
91
|
+
webConnectionId: client.connectionId,
|
|
92
|
+
webConnectionGeneration: client.connectionGeneration,
|
|
93
|
+
digest,
|
|
94
|
+
kind,
|
|
95
|
+
state: 'pending',
|
|
96
|
+
response: null,
|
|
97
|
+
expiresAt: Date.now() + (kind === 'browser_runtime_install'
|
|
98
|
+
? INSTALL_REQUEST_TTL_MS
|
|
99
|
+
: CREATE_REQUEST_TTL_MS),
|
|
100
|
+
};
|
|
101
|
+
browserRequests.set(requestKey, request);
|
|
102
|
+
return { request };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function findBrowserRequest(agentId, serverRequestId) {
|
|
106
|
+
for (const request of browserRequests.values()) {
|
|
107
|
+
if (request.agentId === agentId && request.serverRequestId === serverRequestId) return request;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function completeBrowserRequest(agentId, msg, { consume = false, kinds = null } = {}) {
|
|
113
|
+
const allowedKinds = kinds == null ? null : new Set(Array.isArray(kinds) ? kinds : [kinds]);
|
|
114
|
+
for (const [requestKey, request] of browserRequests) {
|
|
115
|
+
if (request.agentId !== agentId || request.serverRequestId !== msg.requestId || request.state !== 'pending') continue;
|
|
116
|
+
if (allowedKinds && !allowedKinds.has(request.kind)) continue;
|
|
117
|
+
request.state = msg.type.endsWith('_error') ? 'failed' : 'completed';
|
|
118
|
+
request.response = { ...msg, requestId: request.requestId };
|
|
119
|
+
request.expiresAt = Date.now() + CREATE_REQUEST_TTL_MS;
|
|
120
|
+
if (consume) browserRequests.delete(requestKey);
|
|
121
|
+
return request;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function registerBrowserRequest({ agentId, client, requestId, kind, browserSessionId = null }) {
|
|
127
|
+
pruneBrowserRuntimeRoutes();
|
|
128
|
+
const requestKey = createKey(agentId, client.connectionId, requestId);
|
|
129
|
+
if (browserRequests.has(requestKey) || browserRequests.size >= MAX_CREATE_REQUESTS) return null;
|
|
130
|
+
const request = {
|
|
131
|
+
agentId,
|
|
132
|
+
requestId,
|
|
133
|
+
serverRequestId: randomUUID(),
|
|
134
|
+
kind,
|
|
135
|
+
browserSessionId,
|
|
136
|
+
clientId: client.id,
|
|
137
|
+
ownerUserId: client.userId,
|
|
138
|
+
webConnectionId: client.connectionId,
|
|
139
|
+
webConnectionGeneration: client.connectionGeneration,
|
|
140
|
+
digest: '',
|
|
141
|
+
state: 'pending',
|
|
142
|
+
response: null,
|
|
143
|
+
expiresAt: Date.now() + CREATE_REQUEST_TTL_MS,
|
|
144
|
+
};
|
|
145
|
+
browserRequests.set(requestKey, request);
|
|
146
|
+
return request;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function installBrowserRoute({ agentId, ownerUserId, msg }) {
|
|
150
|
+
if (!msg.browserSessionId) return null;
|
|
151
|
+
const routeKey = key(agentId, msg.browserSessionId);
|
|
152
|
+
const existing = browserRoutes.get(routeKey);
|
|
153
|
+
if (existing && existing.ownerUserId !== ownerUserId) return null;
|
|
154
|
+
if (!existing && browserRoutes.size >= MAX_ROUTES) return null;
|
|
155
|
+
const route = {
|
|
156
|
+
ownerUserId,
|
|
157
|
+
agentId,
|
|
158
|
+
browserSessionId: msg.browserSessionId,
|
|
159
|
+
revision: Number(msg.revision) || 1,
|
|
160
|
+
state: msg.state || 'ready',
|
|
161
|
+
updatedAt: Date.now(),
|
|
162
|
+
};
|
|
163
|
+
browserRoutes.set(routeKey, route);
|
|
164
|
+
return route;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getBrowserRoute(agentId, browserSessionId) {
|
|
168
|
+
return browserRoutes.get(key(agentId, browserSessionId)) || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function deleteBrowserRoute(agentId, browserSessionId) {
|
|
172
|
+
const routeKey = key(agentId, browserSessionId);
|
|
173
|
+
const deleted = browserRoutes.delete(routeKey);
|
|
174
|
+
for (const [peerId, peer] of browserPeers) {
|
|
175
|
+
if (peer.agentId === agentId && peer.browserSessionId === browserSessionId) browserPeers.delete(peerId);
|
|
176
|
+
}
|
|
177
|
+
return deleted;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function reserveBrowserPeer({ agentId, browserSessionId, client, requestId, connectionGeneration, role = 'viewer' }) {
|
|
181
|
+
pruneBrowserRuntimeRoutes();
|
|
182
|
+
const route = getBrowserRoute(agentId, browserSessionId);
|
|
183
|
+
if (!route || route.ownerUserId !== client.userId || route.state !== 'ready') return { error: 'browser_session_not_found' };
|
|
184
|
+
if (!Number.isSafeInteger(connectionGeneration) || connectionGeneration <= 0) return { error: 'browser_generation_invalid' };
|
|
185
|
+
const duplicate = [...browserPeers.values()].find(peer => (
|
|
186
|
+
peer.agentId === agentId
|
|
187
|
+
&& peer.browserSessionId === browserSessionId
|
|
188
|
+
&& peer.clientId === client.id
|
|
189
|
+
&& peer.webConnectionId === client.connectionId
|
|
190
|
+
&& peer.requestId === requestId
|
|
191
|
+
&& peer.connectionGeneration === connectionGeneration
|
|
192
|
+
));
|
|
193
|
+
if (duplicate) return { peer: duplicate, duplicate: true };
|
|
194
|
+
if (browserPeers.size >= MAX_PEERS) return { error: 'browser_peer_capacity' };
|
|
195
|
+
const peerId = randomUUID();
|
|
196
|
+
const expiresAt = Date.now() + CONFIG.browserRuntime.routeTtlMs;
|
|
197
|
+
const peer = {
|
|
198
|
+
peerId,
|
|
199
|
+
requestId,
|
|
200
|
+
ownerUserId: client.userId,
|
|
201
|
+
agentId,
|
|
202
|
+
browserSessionId,
|
|
203
|
+
clientId: client.id,
|
|
204
|
+
webConnectionId: client.connectionId,
|
|
205
|
+
webConnectionGeneration: client.connectionGeneration,
|
|
206
|
+
connectionGeneration,
|
|
207
|
+
role: role === 'interactive' ? 'interactive' : 'viewer',
|
|
208
|
+
state: 'preparing',
|
|
209
|
+
pendingOffer: null,
|
|
210
|
+
pendingCandidates: [],
|
|
211
|
+
agentCandidateCount: 0,
|
|
212
|
+
webCandidateCount: 0,
|
|
213
|
+
iceTransportPolicy: CONFIG.browserRuntime.iceTransportPolicy,
|
|
214
|
+
expiresAt,
|
|
215
|
+
};
|
|
216
|
+
browserPeers.set(peerId, peer);
|
|
217
|
+
return { peer };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function getBrowserPeer(peerId) {
|
|
221
|
+
pruneBrowserRuntimeRoutes();
|
|
222
|
+
return browserPeers.get(String(peerId || '')) || null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function browserPeerMatchesClient(peer, client, message = {}) {
|
|
226
|
+
return !!peer && !!client
|
|
227
|
+
&& peer.ownerUserId === client.userId
|
|
228
|
+
&& peer.clientId === client.id
|
|
229
|
+
&& peer.webConnectionId === client.connectionId
|
|
230
|
+
&& peer.webConnectionGeneration === client.connectionGeneration
|
|
231
|
+
&& peer.agentId === message.agentId
|
|
232
|
+
&& peer.browserSessionId === message.browserSessionId
|
|
233
|
+
&& peer.connectionGeneration === Number(message.connectionGeneration);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function deleteBrowserPeer(peerId) {
|
|
237
|
+
return browserPeers.delete(String(peerId || ''));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function clearBrowserRuntimeForClient(client) {
|
|
241
|
+
if (!client) return [];
|
|
242
|
+
const peers = [];
|
|
243
|
+
for (const [peerId, peer] of browserPeers) {
|
|
244
|
+
if (peer.clientId === client.id && peer.webConnectionId === client.connectionId) {
|
|
245
|
+
browserPeers.delete(peerId);
|
|
246
|
+
peers.push(peer);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const [requestKey, request] of browserRequests) {
|
|
250
|
+
if (request.clientId === client.id && request.webConnectionId === client.connectionId) {
|
|
251
|
+
browserRequests.delete(requestKey);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return peers;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function clearBrowserRuntimeForAgent(agentId) {
|
|
258
|
+
for (const [routeKey, route] of browserRoutes) {
|
|
259
|
+
if (route.agentId === agentId) browserRoutes.delete(routeKey);
|
|
260
|
+
}
|
|
261
|
+
for (const [peerId, peer] of browserPeers) {
|
|
262
|
+
if (peer.agentId === agentId) browserPeers.delete(peerId);
|
|
263
|
+
}
|
|
264
|
+
for (const [requestKey, request] of browserRequests) {
|
|
265
|
+
if (request.agentId === agentId) browserRequests.delete(requestKey);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function browserClientForPeer(peer) {
|
|
270
|
+
const client = webClients.get(peer?.clientId);
|
|
271
|
+
if (!client || client.connectionId !== peer.webConnectionId
|
|
272
|
+
|| client.connectionGeneration !== peer.webConnectionGeneration) return null;
|
|
273
|
+
return client;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function browserAgentForPeer(peer) {
|
|
277
|
+
return agents.get(peer?.agentId) || null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function __testResetBrowserRuntimeRoutes() {
|
|
281
|
+
browserRoutes.clear();
|
|
282
|
+
browserPeers.clear();
|
|
283
|
+
browserRequests.clear();
|
|
284
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export const WORKBENCH_ROUTE_PROTOCOL = 1;
|
|
2
|
+
export const BROWSER_RUNTIME_PROTOCOL = 1;
|
|
3
|
+
export const BROWSER_RUNTIME_SETUP_PROTOCOL = 1;
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Apply the explicit browser protocol hello to one Server-owned client record.
|
|
@@ -10,5 +12,11 @@ export function applyClientHello(client, message) {
|
|
|
10
12
|
if (message.workbenchRouteProtocol === WORKBENCH_ROUTE_PROTOCOL) {
|
|
11
13
|
client.workbenchRouteProtocol = WORKBENCH_ROUTE_PROTOCOL;
|
|
12
14
|
}
|
|
15
|
+
if (message.browserRuntimeProtocol === BROWSER_RUNTIME_PROTOCOL) {
|
|
16
|
+
client.browserRuntimeProtocol = BROWSER_RUNTIME_PROTOCOL;
|
|
17
|
+
}
|
|
18
|
+
if (message.browserRuntimeSetupProtocol === BROWSER_RUNTIME_SETUP_PROTOCOL) {
|
|
19
|
+
client.browserRuntimeSetupProtocol = BROWSER_RUNTIME_SETUP_PROTOCOL;
|
|
20
|
+
}
|
|
13
21
|
return true;
|
|
14
22
|
}
|
|
@@ -80,6 +80,10 @@ function loadUsers() {
|
|
|
80
80
|
const DEFAULT_JWT_SECRET = 'default-secret-change-in-production';
|
|
81
81
|
const DEFAULT_AGENT_SECRET = 'agent-shared-secret';
|
|
82
82
|
|
|
83
|
+
function commaList(value) {
|
|
84
|
+
return String(value || '').split(',').map(item => item.trim()).filter(Boolean);
|
|
85
|
+
}
|
|
86
|
+
|
|
83
87
|
export const CONFIG = {
|
|
84
88
|
// Server settings
|
|
85
89
|
port: parseInt(process.env.PORT, 10) || 3456,
|
|
@@ -115,6 +119,19 @@ export const CONFIG = {
|
|
|
115
119
|
// Agent authentication (global fallback — per-user agent_secret is preferred)
|
|
116
120
|
agentSecret: process.env.AGENT_SECRET || DEFAULT_AGENT_SECRET,
|
|
117
121
|
|
|
122
|
+
// Browser Runtime stays fail-closed until the Server rollout gate is enabled.
|
|
123
|
+
// TURN credentials use the standard time-limited HMAC username scheme; Web and
|
|
124
|
+
// Agent endpoints receive separately scoped usernames from the route ledger.
|
|
125
|
+
browserRuntime: {
|
|
126
|
+
enabled: process.env.BROWSER_RUNTIME_ENABLED === 'true',
|
|
127
|
+
iceTransportPolicy: process.env.BROWSER_ICE_TRANSPORT_POLICY === 'relay' ? 'relay' : 'all',
|
|
128
|
+
stunUrls: commaList(process.env.BROWSER_STUN_URLS),
|
|
129
|
+
turnUrls: commaList(process.env.BROWSER_TURN_URLS),
|
|
130
|
+
turnSecret: process.env.BROWSER_TURN_SECRET || '',
|
|
131
|
+
credentialTtlSeconds: Math.min(3600, Math.max(60, parseInt(process.env.BROWSER_TURN_TTL_SECONDS, 10) || 600)),
|
|
132
|
+
routeTtlMs: Math.min(60 * 60_000, Math.max(60_000, parseInt(process.env.BROWSER_ROUTE_TTL_MS, 10) || 15 * 60_000)),
|
|
133
|
+
},
|
|
134
|
+
|
|
118
135
|
// A Sandbox is an ordinary yeaft-agent container managed by this Server's Docker daemon.
|
|
119
136
|
// The Server controls only the container lifecycle; Agent behavior stays on the existing wire.
|
|
120
137
|
sandbox: {
|
|
@@ -273,6 +290,17 @@ export function validateProductionConfig() {
|
|
|
273
290
|
if (CONFIG.sandbox.enabled && !/^wss?:\/\//.test(CONFIG.sandbox.serverUrl)) {
|
|
274
291
|
errors.push('SANDBOX_SERVER_URL must be the ws:// or wss:// URL that container Agents use to connect');
|
|
275
292
|
}
|
|
293
|
+
if (CONFIG.browserRuntime.enabled) {
|
|
294
|
+
const invalidIceUrl = [...CONFIG.browserRuntime.stunUrls, ...CONFIG.browserRuntime.turnUrls]
|
|
295
|
+
.find(url => !/^(?:stun|stuns|turn|turns):/i.test(url));
|
|
296
|
+
if (invalidIceUrl) errors.push(`Invalid Browser Runtime ICE URL: ${invalidIceUrl}`);
|
|
297
|
+
if (CONFIG.browserRuntime.turnUrls.length > 0 && !CONFIG.browserRuntime.turnSecret) {
|
|
298
|
+
errors.push('BROWSER_TURN_SECRET is required when BROWSER_TURN_URLS is configured');
|
|
299
|
+
}
|
|
300
|
+
if (CONFIG.browserRuntime.iceTransportPolicy === 'relay' && CONFIG.browserRuntime.turnUrls.length === 0) {
|
|
301
|
+
errors.push('BROWSER_TURN_URLS is required when BROWSER_ICE_TRANSPORT_POLICY=relay');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
276
304
|
|
|
277
305
|
// Check that at least one user with a password exists (in DB or config)
|
|
278
306
|
// Only warn (don't block startup) — allows first-time setup via create-user.js
|