@wrongstack/webui 0.8.4 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/entry.js +349 -10
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.js +349 -10
- package/dist/server/index.js.map +1 -1
- package/package.json +5 -5
package/dist/server/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/sec
|
|
|
32
32
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
|
|
33
33
|
import { builtinToolsPack, forgetTool, rememberTool } from "@wrongstack/tools";
|
|
34
34
|
import { WebSocket, WebSocketServer } from "ws";
|
|
35
|
-
import { randomBytes } from "crypto";
|
|
35
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
36
36
|
|
|
37
37
|
// ../runtime/src/container.ts
|
|
38
38
|
import {
|
|
@@ -134,7 +134,310 @@ function patchConfig(config, updates) {
|
|
|
134
134
|
return Object.freeze({ ...config, ...updates });
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// src/server/autophase-ws-handler.ts
|
|
138
|
+
import {
|
|
139
|
+
AutoPhasePlanner,
|
|
140
|
+
PhaseGraphBuilder,
|
|
141
|
+
PhaseOrchestrator,
|
|
142
|
+
PhaseStore
|
|
143
|
+
} from "@wrongstack/core";
|
|
144
|
+
var AutoPhaseWebSocketHandler = class {
|
|
145
|
+
constructor(agent, context, logger, storeDir) {
|
|
146
|
+
this.agent = agent;
|
|
147
|
+
this.context = context;
|
|
148
|
+
this.logger = logger;
|
|
149
|
+
this.store = new PhaseStore({ baseDir: storeDir });
|
|
150
|
+
}
|
|
151
|
+
agent;
|
|
152
|
+
context;
|
|
153
|
+
logger;
|
|
154
|
+
orchestrator = null;
|
|
155
|
+
graph = null;
|
|
156
|
+
store;
|
|
157
|
+
clients = /* @__PURE__ */ new Set();
|
|
158
|
+
broadcastInterval = null;
|
|
159
|
+
/** Aborts in-flight task agents when the run is stopped. */
|
|
160
|
+
abort = null;
|
|
161
|
+
addClient(ws) {
|
|
162
|
+
const client = { ws, id: crypto.randomUUID() };
|
|
163
|
+
this.clients.add(client);
|
|
164
|
+
ws.on("close", () => this.clients.delete(client));
|
|
165
|
+
ws.on("error", () => this.clients.delete(client));
|
|
166
|
+
this.sendState(client);
|
|
167
|
+
}
|
|
168
|
+
async handleMessage(msg) {
|
|
169
|
+
switch (msg.type) {
|
|
170
|
+
case "autophase.start":
|
|
171
|
+
await this.handleStart(msg.payload);
|
|
172
|
+
break;
|
|
173
|
+
case "autophase.pause":
|
|
174
|
+
this.orchestrator?.pause();
|
|
175
|
+
this.broadcast({ type: "autophase.paused", payload: {} });
|
|
176
|
+
break;
|
|
177
|
+
case "autophase.resume":
|
|
178
|
+
this.orchestrator?.resume();
|
|
179
|
+
this.broadcast({ type: "autophase.resumed", payload: {} });
|
|
180
|
+
break;
|
|
181
|
+
case "autophase.stop":
|
|
182
|
+
this.abort?.abort();
|
|
183
|
+
this.orchestrator?.stop();
|
|
184
|
+
this.stopBroadcast();
|
|
185
|
+
if (this.graph) void this.store.save(this.graph);
|
|
186
|
+
this.broadcast({ type: "autophase.stopped", payload: {} });
|
|
187
|
+
break;
|
|
188
|
+
case "autophase.status":
|
|
189
|
+
this.broadcastState();
|
|
190
|
+
break;
|
|
191
|
+
case "autophase.selectPhase": {
|
|
192
|
+
const phaseId = msg.payload?.phaseId;
|
|
193
|
+
if (phaseId && this.graph) {
|
|
194
|
+
this.broadcastState(phaseId);
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case "autophase.taskStatus": {
|
|
199
|
+
const { taskId, status } = msg.payload;
|
|
200
|
+
await this.handleTaskStatusChange(taskId, status);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "autophase.toggleAutonomous": {
|
|
204
|
+
const autonomous = msg.payload?.autonomous ?? !this.graph?.autonomous;
|
|
205
|
+
if (this.graph) {
|
|
206
|
+
this.graph.autonomous = autonomous;
|
|
207
|
+
await this.store.save(this.graph);
|
|
208
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case "autophase.save": {
|
|
213
|
+
if (this.graph) {
|
|
214
|
+
await this.store.save(this.graph);
|
|
215
|
+
this.broadcast({ type: "autophase.saved", payload: { graphId: this.graph.id } });
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case "autophase.list": {
|
|
220
|
+
const graphs = await this.store.list();
|
|
221
|
+
this.broadcast({ type: "autophase.list", payload: { graphs } });
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "autophase.load": {
|
|
225
|
+
const graphId = msg.payload?.graphId;
|
|
226
|
+
if (graphId) {
|
|
227
|
+
const graph = await this.store.load(graphId);
|
|
228
|
+
if (graph) {
|
|
229
|
+
this.graph = graph;
|
|
230
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
231
|
+
} else {
|
|
232
|
+
this.broadcast({ type: "autophase.error", payload: { message: `Graph not found: ${graphId}` } });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async handleStart(payload) {
|
|
240
|
+
const title = payload?.goal || payload?.title || "Untitled Project";
|
|
241
|
+
const autonomous = payload?.autonomous ?? true;
|
|
242
|
+
const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(title);
|
|
243
|
+
this.logger.info(`[AutoPhase] Starting: ${title}`);
|
|
244
|
+
const graph = await new PhaseGraphBuilder({ title, phases, autonomous }).build();
|
|
245
|
+
this.graph = graph;
|
|
246
|
+
this.abort = new AbortController();
|
|
247
|
+
await this.store.save(graph);
|
|
248
|
+
this.orchestrator = new PhaseOrchestrator({
|
|
249
|
+
graph,
|
|
250
|
+
ctx: {
|
|
251
|
+
executeTask: async (task, phaseId) => {
|
|
252
|
+
this.logger.info(`[AutoPhase] [${phaseId}] Executing: ${task.title}`);
|
|
253
|
+
const result = await this.executeTaskWithAgent(task, phaseId);
|
|
254
|
+
this.logger.info(`[AutoPhase] [${phaseId}] Completed: ${task.title}`);
|
|
255
|
+
return result;
|
|
256
|
+
},
|
|
257
|
+
onPhaseComplete: (phase) => {
|
|
258
|
+
this.logger.info(`[AutoPhase] Phase completed: ${phase.name}`);
|
|
259
|
+
void this.store.save(graph);
|
|
260
|
+
this.broadcastState();
|
|
261
|
+
},
|
|
262
|
+
onPhaseFail: (phase, error) => {
|
|
263
|
+
this.logger.error(`[AutoPhase] Phase failed: ${phase.name} \u2014 ${error.message}`);
|
|
264
|
+
void this.store.save(graph);
|
|
265
|
+
this.broadcastState();
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
autonomous,
|
|
269
|
+
maxConcurrentPhases: 1,
|
|
270
|
+
// Sequential within a phase: each todo is a full-tool agent editing the
|
|
271
|
+
// shared working tree, so running two at once risks concurrent writes.
|
|
272
|
+
maxConcurrentTasks: 1
|
|
273
|
+
});
|
|
274
|
+
this.startBroadcast();
|
|
275
|
+
this.broadcastState();
|
|
276
|
+
void this.orchestrator.start().then(() => {
|
|
277
|
+
this.orchestrator?.stop();
|
|
278
|
+
void this.store.save(graph);
|
|
279
|
+
this.stopBroadcast();
|
|
280
|
+
const failed = graph.failedPhaseIds.length > 0;
|
|
281
|
+
this.broadcast(
|
|
282
|
+
failed ? { type: "autophase.failed", payload: { title } } : { type: "autophase.completed", payload: { title } }
|
|
283
|
+
);
|
|
284
|
+
this.broadcastState();
|
|
285
|
+
}).catch((err) => {
|
|
286
|
+
this.logger.error(`[AutoPhase] Aborted: ${err instanceof Error ? err.message : String(err)}`);
|
|
287
|
+
this.stopBroadcast();
|
|
288
|
+
this.broadcast({ type: "autophase.failed", payload: { title, error: String(err) } });
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/** Generic fallback phases when the LLM planner produces nothing usable. */
|
|
292
|
+
defaultPhases() {
|
|
293
|
+
return [
|
|
294
|
+
{ name: "Discovery", description: "Requirements gathering", priority: "high", estimateHours: 2, parallelizable: false },
|
|
295
|
+
{ name: "Design", description: "Architecture and design", priority: "critical", estimateHours: 4, parallelizable: false },
|
|
296
|
+
{ name: "Implementation", description: "Core development", priority: "critical", estimateHours: 12, parallelizable: false },
|
|
297
|
+
{ name: "Testing", description: "Unit and integration tests", priority: "high", estimateHours: 6, parallelizable: true },
|
|
298
|
+
{ name: "Deployment", description: "Deploy to production", priority: "medium", estimateHours: 2, parallelizable: false }
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure. */
|
|
302
|
+
async planPhases(goal) {
|
|
303
|
+
try {
|
|
304
|
+
const planner = new AutoPhasePlanner({
|
|
305
|
+
goal,
|
|
306
|
+
runOnce: async (prompt) => {
|
|
307
|
+
const result = await this.agent.run(prompt, { signal: new AbortController().signal });
|
|
308
|
+
return result.status === "done" ? result.finalText ?? "" : "";
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
const { phases, parseFailed } = await planner.plan();
|
|
312
|
+
if (!parseFailed && phases.length > 0) {
|
|
313
|
+
const todos = phases.reduce((n, p) => n + (p.taskTemplates?.length ?? 0), 0);
|
|
314
|
+
this.logger.info(`[AutoPhase] Planned ${phases.length} phases / ${todos} todos for: ${goal}`);
|
|
315
|
+
return phases;
|
|
316
|
+
}
|
|
317
|
+
this.logger.info(`[AutoPhase] Planner produced no phases; using defaults for: ${goal}`);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
this.logger.error(`[AutoPhase] Planning failed, using defaults: ${err instanceof Error ? err.message : String(err)}`);
|
|
320
|
+
}
|
|
321
|
+
return this.defaultPhases();
|
|
322
|
+
}
|
|
323
|
+
async executeTaskWithAgent(task, phaseId) {
|
|
324
|
+
const prompt = `Execute task: ${task.title}
|
|
325
|
+
|
|
326
|
+
Description: ${task.description}
|
|
327
|
+
Phase: ${phaseId}
|
|
328
|
+
Priority: ${task.priority}
|
|
329
|
+
Type: ${task.type}`;
|
|
330
|
+
const signal = this.abort?.signal ?? new AbortController().signal;
|
|
331
|
+
const result = await this.agent.run(prompt, { signal });
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
async handleTaskStatusChange(taskId, status) {
|
|
335
|
+
if (!this.graph) return;
|
|
336
|
+
for (const phase of this.graph.phases.values()) {
|
|
337
|
+
const task = phase.taskGraph.nodes.get(taskId);
|
|
338
|
+
if (task) {
|
|
339
|
+
task.status = status;
|
|
340
|
+
task.updatedAt = Date.now();
|
|
341
|
+
this.broadcastState();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
startBroadcast() {
|
|
347
|
+
if (this.broadcastInterval) return;
|
|
348
|
+
this.broadcastInterval = setInterval(() => {
|
|
349
|
+
const progress = this.orchestrator?.getProgress();
|
|
350
|
+
if (progress) this.broadcast({ type: "autophase.progress", payload: progress });
|
|
351
|
+
this.broadcastState();
|
|
352
|
+
}, 2e3);
|
|
353
|
+
}
|
|
354
|
+
stopBroadcast() {
|
|
355
|
+
if (this.broadcastInterval) {
|
|
356
|
+
clearInterval(this.broadcastInterval);
|
|
357
|
+
this.broadcastInterval = null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
broadcastState(activePhaseId) {
|
|
361
|
+
if (!this.graph) return;
|
|
362
|
+
const state = this.buildState(activePhaseId);
|
|
363
|
+
this.broadcast({ type: "autophase.state", payload: state });
|
|
364
|
+
}
|
|
365
|
+
buildState(activePhaseId) {
|
|
366
|
+
if (!this.graph) {
|
|
367
|
+
return { phases: [], tasks: [], overallPercent: 0, autonomous: true, title: "" };
|
|
368
|
+
}
|
|
369
|
+
const phases = Array.from(this.graph.phases.values());
|
|
370
|
+
const currentActiveId = activePhaseId || phases.find((p) => p.status === "running")?.id || phases[0]?.id || "";
|
|
371
|
+
const activePhase = this.graph.phases.get(currentActiveId);
|
|
372
|
+
const totalTasks = phases.reduce((sum, p) => sum + p.taskGraph.nodes.size, 0);
|
|
373
|
+
const completedTasks = phases.reduce(
|
|
374
|
+
(sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
|
|
375
|
+
0
|
|
376
|
+
);
|
|
377
|
+
const phaseItems = phases.map((p) => ({
|
|
378
|
+
id: p.id,
|
|
379
|
+
name: p.name,
|
|
380
|
+
description: p.description,
|
|
381
|
+
status: p.status,
|
|
382
|
+
priority: p.priority,
|
|
383
|
+
estimateHours: p.estimateHours,
|
|
384
|
+
actualDurationMs: p.actualDurationMs,
|
|
385
|
+
startedAt: p.startedAt,
|
|
386
|
+
completedAt: p.completedAt,
|
|
387
|
+
progressPercent: p.taskGraph.nodes.size > 0 ? Math.round(Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length / p.taskGraph.nodes.size * 100) : 0,
|
|
388
|
+
taskCount: p.taskGraph.nodes.size,
|
|
389
|
+
completedTasks: Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
|
|
390
|
+
assignedAgents: p.assignedAgents,
|
|
391
|
+
isActive: p.id === currentActiveId
|
|
392
|
+
}));
|
|
393
|
+
const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map((t) => ({
|
|
394
|
+
id: t.id,
|
|
395
|
+
title: t.title,
|
|
396
|
+
description: t.description,
|
|
397
|
+
status: t.status,
|
|
398
|
+
priority: t.priority,
|
|
399
|
+
type: t.type,
|
|
400
|
+
estimateHours: t.estimateHours,
|
|
401
|
+
actualHours: t.actualHours,
|
|
402
|
+
assignee: t.assignee,
|
|
403
|
+
tags: t.tags || [],
|
|
404
|
+
startedAt: t.startedAt,
|
|
405
|
+
completedAt: t.completedAt
|
|
406
|
+
})) : [];
|
|
407
|
+
const completedPhases = phases.filter((p) => p.status === "completed").length;
|
|
408
|
+
return {
|
|
409
|
+
title: this.graph.title,
|
|
410
|
+
phases: phaseItems,
|
|
411
|
+
tasks: taskItems,
|
|
412
|
+
activePhaseId: currentActiveId,
|
|
413
|
+
overallPercent: phases.length > 0 ? Math.round(completedPhases / phases.length * 100) : 0,
|
|
414
|
+
autonomous: this.graph.autonomous,
|
|
415
|
+
totalTasks,
|
|
416
|
+
completedTasks
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
sendState(client) {
|
|
420
|
+
if (!this.graph) return;
|
|
421
|
+
const state = this.buildState();
|
|
422
|
+
this.send(client, { type: "autophase.state", payload: state });
|
|
423
|
+
}
|
|
424
|
+
broadcast(msg) {
|
|
425
|
+
const data = JSON.stringify(msg);
|
|
426
|
+
for (const client of this.clients) {
|
|
427
|
+
if (client.ws.readyState === 1) {
|
|
428
|
+
client.ws.send(data);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
send(client, msg) {
|
|
433
|
+
if (client.ws.readyState === 1) {
|
|
434
|
+
client.ws.send(JSON.stringify(msg));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
137
439
|
// src/server/index.ts
|
|
440
|
+
var HTML_CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'";
|
|
138
441
|
async function startWebUI(opts = {}) {
|
|
139
442
|
const wsPort = opts.wsPort ?? 3457;
|
|
140
443
|
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
@@ -336,6 +639,7 @@ async function startWebUI(opts = {}) {
|
|
|
336
639
|
toolExecutor
|
|
337
640
|
});
|
|
338
641
|
console.log("[WebUI] Agent initialized");
|
|
642
|
+
const autoPhaseHandler = new AutoPhaseWebSocketHandler(agent, context, logger, wpaths.projectAutophase);
|
|
339
643
|
async function sessionStartPayload() {
|
|
340
644
|
let maxContext = 0;
|
|
341
645
|
let inputCost = 0;
|
|
@@ -368,12 +672,33 @@ async function startWebUI(opts = {}) {
|
|
|
368
672
|
const wsToken = randomBytes(16).toString("hex");
|
|
369
673
|
console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
|
|
370
674
|
const isLoopback = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
675
|
+
const tokenMatches = (provided) => {
|
|
676
|
+
if (!provided) return false;
|
|
677
|
+
const a = Buffer.from(provided);
|
|
678
|
+
const b = Buffer.from(wsToken);
|
|
679
|
+
if (a.length !== b.length) return false;
|
|
680
|
+
return timingSafeEqual(a, b);
|
|
681
|
+
};
|
|
682
|
+
const hostHeaderOk = (req) => {
|
|
683
|
+
const boundToLoopback = wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
|
|
684
|
+
if (!boundToLoopback) return true;
|
|
685
|
+
const hostHeader = (req.headers.host ?? "").trim();
|
|
686
|
+
if (!hostHeader) return false;
|
|
687
|
+
let hostname;
|
|
688
|
+
try {
|
|
689
|
+
hostname = new URL(`http://${hostHeader}`).hostname;
|
|
690
|
+
} catch {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
return isLoopback(hostname);
|
|
694
|
+
};
|
|
371
695
|
const verifyClient = (info) => {
|
|
372
696
|
const origin = info.origin;
|
|
373
697
|
const url = info.req.url ?? "";
|
|
374
698
|
const tokenMatch = url.match(/[?&]token=([^&]+)/);
|
|
375
699
|
const providedToken = tokenMatch ? tokenMatch[1] : void 0;
|
|
376
|
-
const tokenOk = providedToken
|
|
700
|
+
const tokenOk = tokenMatches(providedToken);
|
|
701
|
+
if (!hostHeaderOk(info.req)) return false;
|
|
377
702
|
if (!origin) {
|
|
378
703
|
const remoteIp = info.req.socket.remoteAddress ?? "";
|
|
379
704
|
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
@@ -383,18 +708,24 @@ async function startWebUI(opts = {}) {
|
|
|
383
708
|
try {
|
|
384
709
|
const { hostname } = new URL(origin);
|
|
385
710
|
if (isLoopback(hostname)) return true;
|
|
386
|
-
if (wsHost === "0.0.0.0") return tokenOk;
|
|
387
711
|
return tokenOk;
|
|
388
712
|
} catch {
|
|
389
713
|
return false;
|
|
390
714
|
}
|
|
391
715
|
};
|
|
716
|
+
const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
|
|
392
717
|
const wssPrimary = new WebSocketServer({
|
|
393
718
|
port: wsPort,
|
|
394
719
|
host: wsHost,
|
|
395
|
-
verifyClient
|
|
720
|
+
verifyClient,
|
|
721
|
+
maxPayload: WS_MAX_PAYLOAD
|
|
396
722
|
});
|
|
397
|
-
const wssSecondary = wsHost === "127.0.0.1" ? new WebSocketServer({
|
|
723
|
+
const wssSecondary = wsHost === "127.0.0.1" ? new WebSocketServer({
|
|
724
|
+
port: wsPort,
|
|
725
|
+
host: "::1",
|
|
726
|
+
verifyClient,
|
|
727
|
+
maxPayload: WS_MAX_PAYLOAD
|
|
728
|
+
}) : null;
|
|
398
729
|
const clients = /* @__PURE__ */ new Map();
|
|
399
730
|
const RATE_LIMIT_MESSAGES = 60;
|
|
400
731
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
@@ -529,6 +860,7 @@ async function startWebUI(opts = {}) {
|
|
|
529
860
|
void sessionStartPayload().then((payload) => {
|
|
530
861
|
send(ws, { type: "session.start", payload });
|
|
531
862
|
});
|
|
863
|
+
autoPhaseHandler.addClient(ws);
|
|
532
864
|
ws.on("message", async (data) => {
|
|
533
865
|
if (!checkRateLimit(ws)) {
|
|
534
866
|
send(ws, {
|
|
@@ -1341,6 +1673,12 @@ async function startWebUI(opts = {}) {
|
|
|
1341
1673
|
});
|
|
1342
1674
|
break;
|
|
1343
1675
|
}
|
|
1676
|
+
default:
|
|
1677
|
+
if (msg.type.startsWith("autophase.")) {
|
|
1678
|
+
await autoPhaseHandler.handleMessage(msg);
|
|
1679
|
+
} else {
|
|
1680
|
+
send(ws, { type: "error", payload: { phase: "handleMessage", message: `Unknown message type: ${msg.type}` } });
|
|
1681
|
+
}
|
|
1344
1682
|
}
|
|
1345
1683
|
}
|
|
1346
1684
|
async function loadSavedProviders() {
|
|
@@ -1537,10 +1875,7 @@ async function startWebUI(opts = {}) {
|
|
|
1537
1875
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1538
1876
|
if (ext === ".html") {
|
|
1539
1877
|
res.setHeader("Cache-Control", "no-cache");
|
|
1540
|
-
res.setHeader(
|
|
1541
|
-
"Content-Security-Policy",
|
|
1542
|
-
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:; img-src 'self' data:; font-src 'self' data:"
|
|
1543
|
-
);
|
|
1878
|
+
res.setHeader("Content-Security-Policy", HTML_CSP);
|
|
1544
1879
|
}
|
|
1545
1880
|
const fileContent = await fs2.readFile(resolvedPath);
|
|
1546
1881
|
res.writeHead(200);
|
|
@@ -1552,7 +1887,11 @@ async function startWebUI(opts = {}) {
|
|
|
1552
1887
|
res.writeHead(200, {
|
|
1553
1888
|
"Content-Type": "text/html",
|
|
1554
1889
|
"X-Content-Type-Options": "nosniff",
|
|
1555
|
-
"X-Frame-Options": "DENY"
|
|
1890
|
+
"X-Frame-Options": "DENY",
|
|
1891
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
1892
|
+
// SPA fallback previously shipped no CSP — apply the same policy as
|
|
1893
|
+
// the direct .html branch so deep-linked routes aren't unprotected.
|
|
1894
|
+
"Content-Security-Policy": HTML_CSP
|
|
1556
1895
|
});
|
|
1557
1896
|
res.end(fileContent);
|
|
1558
1897
|
} catch {
|