@wrongstack/cli 1.0.24 → 1.0.26
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/data/providers.json +44 -0
- package/dist/{acp-AJWSKWE5.js → acp-LI665LO2.js} +5 -5
- package/dist/ascii-mode-SG4PRNLP.js +7 -0
- package/dist/{auth-GRBZ7Y3D.js → auth-TE55JF4Z.js} +14 -14
- package/dist/{bench-N5K7Z5O3.js → bench-GTLPM22B.js} +2 -2
- package/dist/boot/append-system-prompt.d.ts +10 -0
- package/dist/boot/ascii-mode.d.ts +12 -0
- package/dist/boot/container-wiring.d.ts +4 -2
- package/dist/boot/dispatch-singleshot.d.ts +4 -1
- package/dist/boot/mcp-config-flag.d.ts +22 -0
- package/dist/boot/piped-stdin.d.ts +40 -0
- package/dist/boot/restricted-mode.d.ts +15 -0
- package/dist/boot/safe-mode.d.ts +30 -0
- package/dist/boot/single-shot-budget.d.ts +30 -0
- package/dist/boot/stream-json.d.ts +36 -0
- package/dist/boot/structured-output.d.ts +16 -0
- package/dist/boot/system-prompt-builder.d.ts +2 -0
- package/dist/boot/tool-restriction-flags.d.ts +36 -0
- package/dist/boot/worktree-sessions.d.ts +21 -0
- package/dist/boot-provider-selection.d.ts +67 -0
- package/dist/boot.d.ts +1 -38
- package/dist/{chunk-LFMVHT3R.js → chunk-2ERX3RPN.js} +11 -13
- package/dist/chunk-4TVYGG6G.js +62 -0
- package/dist/chunk-5JYDKG5B.js +154 -0
- package/dist/{chunk-3VXYY5JC.js → chunk-5UZBUV2H.js} +3 -19
- package/dist/chunk-BEHGRNOQ.js +58 -0
- package/dist/chunk-CZTXF6HY.js +140 -0
- package/dist/{chunk-SKPDE62W.js → chunk-EPR42SWM.js} +36 -6
- package/dist/{chunk-YBUCXXYZ.js → chunk-F5DCFAKI.js} +1 -1
- package/dist/chunk-HLSHFXTO.js +97 -0
- package/dist/{chunk-BLD5DSVA.js → chunk-KJS5543T.js} +2 -2
- package/dist/{chunk-QFPQXKCG.js → chunk-KOEO4CJG.js} +21 -6
- package/dist/chunk-LBPLTTZ6.js +105 -0
- package/dist/{chunk-3OCUS3VN.js → chunk-LDCGZJTC.js} +4 -4
- package/dist/chunk-LKPEBZBE.js +143 -0
- package/dist/{chunk-DRM7PSW7.js → chunk-OAVBM55Q.js} +3 -3
- package/dist/chunk-PZQBGUVZ.js +18 -0
- package/dist/{chunk-MHEBWVXL.js → chunk-Q4INH2DG.js} +46 -2
- package/dist/{chunk-ZZRSG7VK.js → chunk-RZ4ZDRYA.js} +3 -3
- package/dist/{chunk-U46QR4WR.js → chunk-TI7KLHYX.js} +132 -3
- package/dist/{chunk-PGKJ2OQP.js → chunk-WQV2NP57.js} +5 -5
- package/dist/chunk-X756ZS72.js +329 -0
- package/dist/{chunk-JF2MULWF.js → chunk-YHYINL47.js} +47 -15
- package/dist/{cli-context-34Q5IJRA.js → cli-context-UVGAN3NO.js} +407 -327
- package/dist/{cli-main-AELJDBGU.js → cli-main-TN3KZ2P6.js} +2652 -1978
- package/dist/cli-update-notice.d.ts +2 -1
- package/dist/config-doctor.d.ts +1 -1
- package/dist/{diag-doctor-WIFJH3XH.js → diag-doctor-BIZAFIAT.js} +2 -2
- package/dist/{execution-PYC5FCS4.js → execution-7QIP6GRY.js} +1948 -1666
- package/dist/fleet/host-types.d.ts +3 -1
- package/dist/goal-commands.d.ts +47 -0
- package/dist/goal-host.d.ts +1 -14
- package/dist/{hq-JTUTFI4L.js → hq-GLZ7QESM.js} +6 -6
- package/dist/{hq-server-PC7GZMPT.js → hq-server-YTKDVQPY.js} +2 -2
- package/dist/{hq-service-NW56MUP2.js → hq-service-BMMJAHRT.js} +4 -4
- package/dist/import-claude-code-32PCSIQI.js +236 -0
- package/dist/index.js +12 -8
- package/dist/{mailbox-serve-WDOCH6KR.js → mailbox-serve-NW5VZG7R.js} +3 -3
- package/dist/{modeldiag-36OGAL5C.js → modeldiag-36VODVIH.js} +8 -8
- package/dist/{per-subcommand-help-XE77MTGJ.js → per-subcommand-help-QDAQASPZ.js} +2 -2
- package/dist/permission-prompt.d.ts +14 -0
- package/dist/permission-rules-view.d.ts +18 -0
- package/dist/{permissions-CFDLXETM.js → permissions-PL27UHYD.js} +67 -45
- package/dist/{plugin-usage-S5LVGMBW.js → plugin-usage-CIGOOTX3.js} +2 -2
- package/dist/{plugins-RHBIVLI7.js → plugins-HQTXUD6A.js} +4 -2
- package/dist/preflight.d.ts +1 -1
- package/dist/{projects-LMNAGGEU.js → projects-T4CZCA25.js} +3 -6
- package/dist/provider-helpers.d.ts +7 -11
- package/dist/{providers-models-2CFG46RD.js → providers-models-5Z2PJTEW.js} +5 -5
- package/dist/release-asset-download.d.ts +10 -0
- package/dist/release-attestation.d.ts +28 -0
- package/dist/remote/remote-session.d.ts +64 -0
- package/dist/remote/remote-target.d.ts +69 -0
- package/dist/remote-O5WXHLWV.js +561 -0
- package/dist/{replay-Z3FTJEMM.js → replay-DEI566CW.js} +2 -2
- package/dist/services/commit-message.d.ts +2 -1
- package/dist/session-move-P2EC2COA.js +44 -0
- package/dist/session-move.d.ts +20 -0
- package/dist/session-stats.d.ts +10 -0
- package/dist/{sessions-config-IQUH2GXT.js → sessions-config-SXTTLNXA.js} +35 -5
- package/dist/short-circuit-flags-74Y5N6CF.js +10 -0
- package/dist/slash-commands/command-context.d.ts +15 -5
- package/dist/slash-commands/goal-refiner.d.ts +4 -49
- package/dist/slash-commands/mcp.d.ts +2 -1
- package/dist/slash-commands/permissions.d.ts +4 -0
- package/dist/slash-commands/settings-context-mutations.d.ts +5 -0
- package/dist/slash-commands/settings-limits-mutations.d.ts +12 -0
- package/dist/standalone-auto-update-TOTRGCSA.js +249 -0
- package/dist/standalone-auto-update.d.ts +65 -0
- package/dist/standalone-update-LZO3MNLH.js +24 -0
- package/dist/standalone-update.d.ts +37 -1
- package/dist/subcommands/handlers/import-claude-code.d.ts +42 -0
- package/dist/subcommands/handlers/permissions.d.ts +9 -3
- package/dist/subcommands/handlers/remote.d.ts +6 -0
- package/dist/subcommands/handlers/version-help.d.ts +5 -0
- package/dist/{subcommands-ZTSIN7LG.js → subcommands-Q6ORSB2E.js} +2 -2
- package/dist/tui-resume-callback.d.ts +18 -0
- package/dist/{typesafe-HVEVPA6R.js → typesafe-U7YB2RRV.js} +5 -5
- package/dist/{typesafe-topic-replay-4W73BOLV.js → typesafe-topic-replay-ZILIU3WS.js} +18 -2
- package/dist/{update-7XIOEXTD.js → update-TDZX4IL2.js} +4 -4
- package/dist/{update-check-2POMBHFB.js → update-check-J5ZVKS73.js} +3 -3
- package/dist/version-JB2UAV2A.js +11 -0
- package/dist/version-help-XJJQ7YSH.js +10 -0
- package/dist/webui-server/route-contexts.d.ts +1 -1
- package/dist/webui-server/session-start-payload.d.ts +5 -1
- package/dist/{webui-server-L2SICPTB.js → webui-server-YTHLRFD2.js} +509 -366
- package/dist/webui-server-lifecycle.d.ts +57 -0
- package/dist/webui-server.d.ts +0 -14
- package/dist/wiring/brain-and-orchestration.d.ts +3 -1
- package/dist/wiring/cli-prompt-and-tools-setup.d.ts +9 -0
- package/dist/wiring/cli-slash-commands-setup.d.ts +1 -0
- package/dist/wiring/lifecycle-plugins.d.ts +4 -2
- package/dist/wiring/metrics.d.ts +9 -1
- package/dist/wiring/pipeline.d.ts +2 -7
- package/dist/wiring/plugins.d.ts +3 -0
- package/dist/wiring/provider-status.d.ts +1 -0
- package/dist/wiring/provider-utility-tools.d.ts +3 -1
- package/package.json +28 -28
- package/dist/chunk-2BKFWIER.js +0 -98
- package/dist/chunk-424ORSE7.js +0 -56
- package/dist/chunk-QBBF7GIK.js +0 -65
- package/dist/short-circuit-flags-SDT53OML.js +0 -10
- package/dist/standalone-update-MW47J3AK.js +0 -164
- package/dist/version-help-WZ623NWM.js +0 -10
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createKanbanRunMirror
|
|
3
|
-
} from "./chunk-VCYZ3WZV.js";
|
|
4
1
|
import {
|
|
5
2
|
createKanbanSupervisor
|
|
6
3
|
} from "./chunk-R4QGL6GB.js";
|
|
7
4
|
import {
|
|
8
5
|
WEBUI_SESSION_CHILD_CAPABILITIES
|
|
9
6
|
} from "./chunk-ZMX5HRLI.js";
|
|
7
|
+
import {
|
|
8
|
+
createKanbanRunMirror
|
|
9
|
+
} from "./chunk-VCYZ3WZV.js";
|
|
10
10
|
import {
|
|
11
11
|
createHqCommandDispatcher,
|
|
12
12
|
createProjectKanbanAssignHandler,
|
|
@@ -14,14 +14,365 @@ import {
|
|
|
14
14
|
} from "./chunk-2EBVTYJR.js";
|
|
15
15
|
import {
|
|
16
16
|
startCliHqConnection
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-KJS5543T.js";
|
|
18
|
+
import {
|
|
19
|
+
CLI_VERSION
|
|
20
|
+
} from "./chunk-F5DCFAKI.js";
|
|
18
21
|
import {
|
|
19
22
|
loadConfigProviders,
|
|
20
23
|
mutateConfigProviders
|
|
21
24
|
} from "./chunk-LUMETJFO.js";
|
|
25
|
+
|
|
26
|
+
// src/webui-server-lifecycle.ts
|
|
27
|
+
import { toErrorMessage } from "@wrongstack/core/utils/error";
|
|
28
|
+
|
|
29
|
+
// src/webui-server/connection-handler.ts
|
|
30
|
+
import { deriveSessionAgents } from "@wrongstack/core/storage";
|
|
31
|
+
import { buildReplayPayload, decodeProtocolFrame } from "@wrongstack/webui-protocol";
|
|
22
32
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
createConnectionLifecycle,
|
|
34
|
+
verifyClient as verifyWsClient
|
|
35
|
+
} from "@wrongstack/webui-server";
|
|
36
|
+
function createConnectionHandler(deps) {
|
|
37
|
+
const clientHandlers = [
|
|
38
|
+
deps.goalHandler,
|
|
39
|
+
deps.specsHandler,
|
|
40
|
+
deps.sddBoardHandler,
|
|
41
|
+
...deps.sddWizardHandler ? [deps.sddWizardHandler] : [],
|
|
42
|
+
deps.worktreeHandler,
|
|
43
|
+
deps.terminalHandler
|
|
44
|
+
];
|
|
45
|
+
return createConnectionLifecycle({
|
|
46
|
+
clients: deps.clients,
|
|
47
|
+
pendingConfirms: deps.pendingConfirms,
|
|
48
|
+
// ── Auth trust model ────────────────────────────────────────────────
|
|
49
|
+
// Three layered defenses (see verifyClient in @wrongstack/webui-server/ws-auth):
|
|
50
|
+
// 1. DNS-rebinding guard (Host header must be loopback on loopback bind)
|
|
51
|
+
// 2. Shared-token auth via Cookie (ws_token) or URL query param (?token=)
|
|
52
|
+
// 3. Loopback bootstrap: when requireToken=false (default) AND the server
|
|
53
|
+
// is bound to a loopback interface, browser clients bypass token auth
|
|
54
|
+
// entirely — the Origin header being http://localhost is sufficient.
|
|
55
|
+
//
|
|
56
|
+
// COOKIE PERSISTENCE AFTER TOKEN ROTATION:
|
|
57
|
+
// The HttpOnly ws_token cookie, once set by /ws-auth, survives a process
|
|
58
|
+
// restart. On the next process the expectedToken is a fresh random value,
|
|
59
|
+
// so the old cookie alone would fail the token check. However, on a
|
|
60
|
+
// loopback bind with requireToken=false (default), the loopback-bootstrap
|
|
61
|
+
// path at layer 3 accepts the connection before the cookie is ever
|
|
62
|
+
// compared — the old cookie is harmless because no token is needed.
|
|
63
|
+
//
|
|
64
|
+
// This means a token rotation (e.g. the operator sets a new accessToken
|
|
65
|
+
// in config) does NOT invalidate already-distributed cookies for loopback
|
|
66
|
+
// clients. The mitigating factors:
|
|
67
|
+
// - Loopback bind + requireToken=false already trusts any local process
|
|
68
|
+
// that can reach 127.0.0.1 — a trivial perimeter.
|
|
69
|
+
// - A LAN/0.0.0.0 bind with requireToken=true DOES enforce cookie
|
|
70
|
+
// matching against the new expectedToken on every reconnect.
|
|
71
|
+
// If stronger isolation is needed, set requireToken=true or rebind the
|
|
72
|
+
// server to a non-loopback interface with token enforcement.
|
|
73
|
+
authenticate: (ws, req) => {
|
|
74
|
+
const allowed = verifyWsClient({
|
|
75
|
+
origin: req.headers.origin,
|
|
76
|
+
url: req.url ?? "/",
|
|
77
|
+
hostHeader: req.headers.host,
|
|
78
|
+
remoteAddress: req.socket.remoteAddress,
|
|
79
|
+
cookieHeader: req.headers.cookie,
|
|
80
|
+
wsHost: deps.host,
|
|
81
|
+
expectedToken: deps.wsToken,
|
|
82
|
+
requireToken: deps.requireToken,
|
|
83
|
+
allowedHostnames: deps.publicHostnames,
|
|
84
|
+
allowBrowserUrlToken: Boolean(deps.publicWsUrl)
|
|
85
|
+
});
|
|
86
|
+
if (!allowed) ws.close(4003, "Forbidden");
|
|
87
|
+
return allowed;
|
|
88
|
+
},
|
|
89
|
+
createClient: (ws) => ({ ws, sessionId: deps.currentSessionId() }),
|
|
90
|
+
// Enrich parse-failure / unknown-type logs with per-connection identity
|
|
91
|
+
// so operators can identify which client sent a malformed frame. The
|
|
92
|
+
// `connectionId`/`sessionId` fields are read from the `client` argument
|
|
93
|
+
// directly inside `logRejection`; the CLI's `ConnectedClient` only has
|
|
94
|
+
// `sessionId` (no per-socket `connId`), so the verbose payload carries
|
|
95
|
+
// `sessionId` only. Server-wide identity (`agentId`/`projectRoot`) isn't
|
|
96
|
+
// in scope at this handler and is omitted.
|
|
97
|
+
verboseWsLogging: true,
|
|
98
|
+
registerClient: (ws) => {
|
|
99
|
+
for (const handler of clientHandlers) handler.addClient(ws);
|
|
100
|
+
},
|
|
101
|
+
decode: (raw) => decodeProtocolFrame(raw, "client"),
|
|
102
|
+
dispatch: deps.handleMessage,
|
|
103
|
+
send: deps.send,
|
|
104
|
+
sessionPayload: deps.sessionPayload,
|
|
105
|
+
rateLimitMax: deps.rateLimitMax,
|
|
106
|
+
// NOTE — a closed SOCKET is not a stopped run.
|
|
107
|
+
//
|
|
108
|
+
// This used to `abortControllers.delete(client.sessionId)` — a delete
|
|
109
|
+
// WITHOUT an abort, and only for the tab this connection last acted on.
|
|
110
|
+
// Both halves were wrong. The run kept going while `isRunActive` reported
|
|
111
|
+
// `false`, so `session.delete` would happily unlink the journal a live run
|
|
112
|
+
// was still appending to, and the next `user_message` claimed a second
|
|
113
|
+
// lock for the same session and hit `Agent.run() is already in progress`.
|
|
114
|
+
// The other three declared tabs were never touched at all.
|
|
115
|
+
//
|
|
116
|
+
// Doing nothing is the correct behaviour: a background run outlives the
|
|
117
|
+
// tab that started it (the same invariant `retireUndisplayedSessions`
|
|
118
|
+
// honours), and the run's own `finally` releases the lock when it unwinds.
|
|
119
|
+
// A page that comes back is told what is still running by
|
|
120
|
+
// `session.run_state`; a browser that never comes back drains its
|
|
121
|
+
// unanswerable permission prompts through the last-client drain in
|
|
122
|
+
// `connection-lifecycle.ts`, which lets a wedged run finish and release.
|
|
123
|
+
//
|
|
124
|
+
// So there is deliberately NO `onClose` here.
|
|
125
|
+
buildInitialPayload: async () => {
|
|
126
|
+
const payload = { ...await deps.buildSessionStartPayload({}, deps.needsSetup) };
|
|
127
|
+
try {
|
|
128
|
+
const open = deps.openSessionIds?.();
|
|
129
|
+
if (open) payload["openSessionIds"] = [.../* @__PURE__ */ new Set([...open, deps.currentSessionId()])];
|
|
130
|
+
} catch {
|
|
131
|
+
console.debug("[WebUI] Failed to list open sessions");
|
|
132
|
+
}
|
|
133
|
+
let replayEvents;
|
|
134
|
+
try {
|
|
135
|
+
const replay = await deps.loadReplay?.();
|
|
136
|
+
if (replay) {
|
|
137
|
+
Object.assign(payload, buildReplayPayload(replay));
|
|
138
|
+
replayEvents = replay.events;
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
console.debug("[WebUI] Failed to load replay");
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const roster = deriveSessionAgents(replayEvents ?? []);
|
|
145
|
+
const sessions = roster.length > 0 ? await deps.loadAgentSessions?.(roster.map((agent) => agent.agentId)) : [];
|
|
146
|
+
if (sessions?.length) payload["agentSessions"] = sessions;
|
|
147
|
+
} catch {
|
|
148
|
+
console.debug("[WebUI] Failed to load agent sessions");
|
|
149
|
+
}
|
|
150
|
+
return payload;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/webui-server/lifecycle.ts
|
|
156
|
+
import {
|
|
157
|
+
announceWebuiReady,
|
|
158
|
+
createWebuiShutdown,
|
|
159
|
+
registerWebuiInstance,
|
|
160
|
+
registerWebuiSignalHandlers
|
|
161
|
+
} from "@wrongstack/webui-server";
|
|
162
|
+
|
|
163
|
+
// src/webui-server/logger-shim.ts
|
|
164
|
+
var structuredLine = (level, message) => JSON.stringify({
|
|
165
|
+
level,
|
|
166
|
+
event: "webui.goal",
|
|
167
|
+
message,
|
|
168
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
169
|
+
});
|
|
170
|
+
var consoleLogger = {
|
|
171
|
+
level: "debug",
|
|
172
|
+
error(msg, _ctx) {
|
|
173
|
+
console.error(structuredLine("error", msg));
|
|
174
|
+
},
|
|
175
|
+
warn(msg, _ctx) {
|
|
176
|
+
console.warn(structuredLine("warn", msg));
|
|
177
|
+
},
|
|
178
|
+
info(msg, _ctx) {
|
|
179
|
+
console.log(structuredLine("info", msg));
|
|
180
|
+
},
|
|
181
|
+
debug(msg, _ctx) {
|
|
182
|
+
console.debug(structuredLine("debug", msg));
|
|
183
|
+
},
|
|
184
|
+
trace(msg, _ctx) {
|
|
185
|
+
console.debug(structuredLine("trace", msg));
|
|
186
|
+
},
|
|
187
|
+
child(_bindings) {
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// src/webui-server-lifecycle.ts
|
|
193
|
+
function runWebuiServerLifecycle({
|
|
194
|
+
host,
|
|
195
|
+
httpPort,
|
|
196
|
+
setupEvents,
|
|
197
|
+
opts,
|
|
198
|
+
wsPort,
|
|
199
|
+
accessUrl,
|
|
200
|
+
wsToken,
|
|
201
|
+
webuiInstanceRegistered,
|
|
202
|
+
wss,
|
|
203
|
+
httpServer,
|
|
204
|
+
requireToken,
|
|
205
|
+
publicHostnames,
|
|
206
|
+
publicWsUrl,
|
|
207
|
+
clients,
|
|
208
|
+
currentSessionId,
|
|
209
|
+
goalHandler,
|
|
210
|
+
specsHandler,
|
|
211
|
+
sddBoardHandler,
|
|
212
|
+
sddWizardHandler,
|
|
213
|
+
worktreeHandler,
|
|
214
|
+
terminalHandler,
|
|
215
|
+
rateLimitMax,
|
|
216
|
+
send,
|
|
217
|
+
sessionPayload,
|
|
218
|
+
handleMessage,
|
|
219
|
+
pendingConfirms,
|
|
220
|
+
buildSessionStartPayload,
|
|
221
|
+
sessionAgents,
|
|
222
|
+
lifecycleState,
|
|
223
|
+
abortControllers,
|
|
224
|
+
flushAllStreamBuffers,
|
|
225
|
+
eventUnsubscribers,
|
|
226
|
+
releaseSessionSalvage,
|
|
227
|
+
autoWakeHost,
|
|
228
|
+
stopEmptySessionCleanup,
|
|
229
|
+
credentialWatcherClose,
|
|
230
|
+
kanbanRunMirror,
|
|
231
|
+
kanbanSupervisor,
|
|
232
|
+
stopKanbanSupervisorMemoryStats,
|
|
233
|
+
unregisterWebuiClient,
|
|
234
|
+
ipv6LoopbackServer,
|
|
235
|
+
registryBaseDir,
|
|
236
|
+
stopLiveStatusLogger,
|
|
237
|
+
terminalLogView
|
|
238
|
+
}) {
|
|
239
|
+
const stopped = new Promise((resolve) => {
|
|
240
|
+
let listeningAnnounced = false;
|
|
241
|
+
const announceListening = () => {
|
|
242
|
+
if (listeningAnnounced) return;
|
|
243
|
+
listeningAnnounced = true;
|
|
244
|
+
console.log(`[WebUI] WebSocket server running on ws://${host}:${httpPort}`);
|
|
245
|
+
try {
|
|
246
|
+
setupEvents();
|
|
247
|
+
opts.onListening?.({
|
|
248
|
+
httpPort,
|
|
249
|
+
wsPort,
|
|
250
|
+
host,
|
|
251
|
+
url: accessUrl,
|
|
252
|
+
authToken: wsToken,
|
|
253
|
+
webuiInstanceRegistered
|
|
254
|
+
});
|
|
255
|
+
} catch (err) {
|
|
256
|
+
consoleLogger.error("setup_events_failed", { message: toErrorMessage(err) });
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
wss.on("listening", announceListening);
|
|
260
|
+
if (httpServer?.server.listening || wss.address()) queueMicrotask(announceListening);
|
|
261
|
+
wss.on(
|
|
262
|
+
"connection",
|
|
263
|
+
createConnectionHandler({
|
|
264
|
+
host,
|
|
265
|
+
wsToken,
|
|
266
|
+
requireToken,
|
|
267
|
+
publicHostnames,
|
|
268
|
+
publicWsUrl,
|
|
269
|
+
clients,
|
|
270
|
+
currentSessionId,
|
|
271
|
+
goalHandler,
|
|
272
|
+
specsHandler,
|
|
273
|
+
sddBoardHandler,
|
|
274
|
+
sddWizardHandler,
|
|
275
|
+
worktreeHandler,
|
|
276
|
+
terminalHandler,
|
|
277
|
+
rateLimitMax,
|
|
278
|
+
send,
|
|
279
|
+
sessionPayload,
|
|
280
|
+
handleMessage,
|
|
281
|
+
pendingConfirms,
|
|
282
|
+
buildSessionStartPayload,
|
|
283
|
+
loadReplay: async () => {
|
|
284
|
+
const activeSession = opts.agent.ctx.session ?? opts.session;
|
|
285
|
+
await activeSession.flush();
|
|
286
|
+
if (opts.sessionStore) {
|
|
287
|
+
const data = await opts.sessionStore.load(activeSession.id);
|
|
288
|
+
return { messages: data.messages, events: data.events, usage: data.usage };
|
|
289
|
+
}
|
|
290
|
+
const usage = opts.agent.ctx.tokenCounter.total();
|
|
291
|
+
return { messages: opts.agent.ctx.messages, usage };
|
|
292
|
+
},
|
|
293
|
+
loadAgentSessions: async (subagentIds) => await opts.agentTranscripts?.loadSessionsFromDisk(subagentIds) ?? [],
|
|
294
|
+
// What this process is actually holding right now. The browser
|
|
295
|
+
// reconciles its persisted tab strip against it, so a restarted server
|
|
296
|
+
// is not dressed in the previous run's tabs.
|
|
297
|
+
openSessionIds: () => sessionAgents.ids(),
|
|
298
|
+
needsSetup: opts.needsSetup ?? false
|
|
299
|
+
})
|
|
300
|
+
);
|
|
301
|
+
lifecycleState.signalShutdown = createWebuiShutdown({
|
|
302
|
+
abortInFlight: () => {
|
|
303
|
+
void lifecycleState.embeddedAutoHealDispose?.();
|
|
304
|
+
for (const c of abortControllers.values()) c.abort();
|
|
305
|
+
abortControllers.clear();
|
|
306
|
+
},
|
|
307
|
+
unsubscribeEvents: () => {
|
|
308
|
+
flushAllStreamBuffers();
|
|
309
|
+
for (const unsub of eventUnsubscribers) unsub();
|
|
310
|
+
},
|
|
311
|
+
stopOwnedChildren: async () => {
|
|
312
|
+
try {
|
|
313
|
+
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
314
|
+
getProcessRegistry().killAll({ force: true, includeProtected: true });
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.debug(
|
|
317
|
+
`[webui-server] process-registry killAll failed: ${err instanceof Error ? err.message : String(err)}`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (opts.mcpRegistry) {
|
|
321
|
+
try {
|
|
322
|
+
await opts.mcpRegistry.stopAll();
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.debug(
|
|
325
|
+
`[webui-server] mcpRegistry.stopAll failed: ${err instanceof Error ? err.message : String(err)}`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
disposeResources: async () => {
|
|
331
|
+
releaseSessionSalvage();
|
|
332
|
+
autoWakeHost?.dispose();
|
|
333
|
+
await stopEmptySessionCleanup?.dispose();
|
|
334
|
+
await sessionAgents.closeAll().catch(() => void 0);
|
|
335
|
+
credentialWatcherClose?.();
|
|
336
|
+
credentialWatcherClose = void 0;
|
|
337
|
+
goalHandler.dispose();
|
|
338
|
+
sddBoardHandler.dispose();
|
|
339
|
+
worktreeHandler.dispose();
|
|
340
|
+
terminalHandler.dispose();
|
|
341
|
+
kanbanRunMirror?.dispose();
|
|
342
|
+
kanbanSupervisor?.dispose();
|
|
343
|
+
void stopKanbanSupervisorMemoryStats?.();
|
|
344
|
+
await lifecycleState.embeddedAutoHealDispose?.();
|
|
345
|
+
lifecycleState.embeddedAutoHealDispose = null;
|
|
346
|
+
unregisterWebuiClient();
|
|
347
|
+
},
|
|
348
|
+
closeClients: () => {
|
|
349
|
+
for (const [ws] of clients) ws.close();
|
|
350
|
+
clients.clear();
|
|
351
|
+
},
|
|
352
|
+
closeHttpServer: () => {
|
|
353
|
+
ipv6LoopbackServer?.close();
|
|
354
|
+
httpServer?.server.close();
|
|
355
|
+
},
|
|
356
|
+
wss,
|
|
357
|
+
pid: process.pid,
|
|
358
|
+
registryBaseDir,
|
|
359
|
+
onStopped: () => {
|
|
360
|
+
stopLiveStatusLogger();
|
|
361
|
+
const muted = terminalLogView.mutedCount;
|
|
362
|
+
terminalLogView.stop();
|
|
363
|
+
if (muted > 0) {
|
|
364
|
+
console.log(
|
|
365
|
+
`[WebUI] ${muted} progress line(s) kept out of this terminal \u2014 set WEBUI_LOGS=1 to stream them.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
opts.onExit?.();
|
|
369
|
+
resolve();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
registerWebuiSignalHandlers(lifecycleState.signalShutdown);
|
|
373
|
+
});
|
|
374
|
+
return { stopped };
|
|
375
|
+
}
|
|
25
376
|
|
|
26
377
|
// src/webui-server.ts
|
|
27
378
|
import * as path7 from "node:path";
|
|
@@ -31,7 +382,6 @@ import {
|
|
|
31
382
|
startSharedHeapWatchdog,
|
|
32
383
|
wstackGlobalRoot
|
|
33
384
|
} from "@wrongstack/core/utils";
|
|
34
|
-
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils/error";
|
|
35
385
|
import {
|
|
36
386
|
buildWebUIAccessUrl,
|
|
37
387
|
clientWantsSession,
|
|
@@ -51,7 +401,8 @@ import {
|
|
|
51
401
|
sendSerialized,
|
|
52
402
|
stampDispatchSession,
|
|
53
403
|
startTerminalDashboard,
|
|
54
|
-
toSessionHistoryEntries
|
|
404
|
+
toSessionHistoryEntries,
|
|
405
|
+
webuiSessionFrameLog
|
|
55
406
|
} from "@wrongstack/webui-server";
|
|
56
407
|
import { verifyClient as verifyWsClient2 } from "@wrongstack/webui-server/server/ws-auth";
|
|
57
408
|
import { WebSocketServer } from "ws";
|
|
@@ -136,182 +487,56 @@ function createEmbeddedClientRegistration(opts, clients, abortControllers, getSe
|
|
|
136
487
|
getSessionWriter: (sessionId) => getSessionAgents()?.peek(sessionId)?.ctx.session,
|
|
137
488
|
hqControl: {
|
|
138
489
|
// HQ speaks for the LEADER — the boot session, the one it registered
|
|
139
|
-
// itself under (`hqSessionId`) — not for whatever else the browser
|
|
140
|
-
// has open. This used to abort every controller in the map and clear
|
|
141
|
-
// it, so a remote "interrupt" issued against the leader also killed
|
|
142
|
-
// the three other tabs' in-flight runs. Deleting the entries was
|
|
143
|
-
// wrong on its own terms too: the run's own `end()` owns removal, and
|
|
144
|
-
// clearing early makes `isRunActive` lie to every tab still running.
|
|
145
|
-
// Every open tab is its own session with its own abort controller, so
|
|
146
|
-
// the command's session is the one that gets stopped. Falling back to
|
|
147
|
-
// the boot session keeps a dashboard that sends no session — every one
|
|
148
|
-
// before this existed — behaving exactly as before.
|
|
149
|
-
interruptLeader: (sessionId) => {
|
|
150
|
-
const leaderId = sessionId ?? opts.session.id;
|
|
151
|
-
const controller = abortControllers.get(leaderId);
|
|
152
|
-
if (!controller) return false;
|
|
153
|
-
controller.abort();
|
|
154
|
-
try {
|
|
155
|
-
void Promise.resolve(opts.stopSessionFleet?.(leaderId)).catch(() => void 0);
|
|
156
|
-
} catch {
|
|
157
|
-
}
|
|
158
|
-
return true;
|
|
159
|
-
},
|
|
160
|
-
allowRunCommand: () => opts.hqAllowExec === true,
|
|
161
|
-
// A command naming a tab this process no longer holds is refused, not
|
|
162
|
-
// redirected onto the boot session: the operator picked a terminal,
|
|
163
|
-
// and steering a different one is worse than not steering at all.
|
|
164
|
-
ownsSession: (sessionId) => sessionId === opts.session.id || getSessionAgents()?.has(sessionId) === true
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
function startEmbeddedLiveStatusLogger(opts, clients, abortControllers, getSessionAgents, terminalLogView) {
|
|
169
|
-
return startWebUILiveStatusLogger({
|
|
170
|
-
events: opts.events,
|
|
171
|
-
dashboard: terminalLogView,
|
|
172
|
-
getSessionList: () => {
|
|
173
|
-
const ids = /* @__PURE__ */ new Set();
|
|
174
|
-
for (const client of clients.values()) {
|
|
175
|
-
if (client.sessionId) ids.add(client.sessionId);
|
|
176
|
-
for (const id of client.sessionIds ?? []) ids.add(id);
|
|
177
|
-
}
|
|
178
|
-
const currentId = opts.agent.ctx.session?.id ?? opts.session.id;
|
|
179
|
-
if (ids.size === 0 && currentId) ids.add(currentId);
|
|
180
|
-
return Array.from(ids).map((id) => {
|
|
181
|
-
const ctx = getSessionAgents()?.peek(id)?.ctx;
|
|
182
|
-
return {
|
|
183
|
-
id,
|
|
184
|
-
model: ctx?.model ?? opts.agent.ctx.model ?? "",
|
|
185
|
-
provider: ctx?.provider?.id ?? opts.agent.ctx.provider?.id ?? "",
|
|
186
|
-
isRunning: abortControllers.has(id)
|
|
187
|
-
};
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// src/webui-server/connection-handler.ts
|
|
194
|
-
import { deriveSessionAgents } from "@wrongstack/core/storage";
|
|
195
|
-
import { buildReplayPayload, decodeProtocolFrame } from "@wrongstack/webui-protocol";
|
|
196
|
-
import {
|
|
197
|
-
createConnectionLifecycle,
|
|
198
|
-
verifyClient as verifyWsClient
|
|
199
|
-
} from "@wrongstack/webui-server";
|
|
200
|
-
function createConnectionHandler(deps) {
|
|
201
|
-
const clientHandlers = [
|
|
202
|
-
deps.goalHandler,
|
|
203
|
-
deps.specsHandler,
|
|
204
|
-
deps.sddBoardHandler,
|
|
205
|
-
...deps.sddWizardHandler ? [deps.sddWizardHandler] : [],
|
|
206
|
-
deps.worktreeHandler,
|
|
207
|
-
deps.terminalHandler
|
|
208
|
-
];
|
|
209
|
-
return createConnectionLifecycle({
|
|
210
|
-
clients: deps.clients,
|
|
211
|
-
pendingConfirms: deps.pendingConfirms,
|
|
212
|
-
// ── Auth trust model ────────────────────────────────────────────────
|
|
213
|
-
// Three layered defenses (see verifyClient in @wrongstack/webui-server/ws-auth):
|
|
214
|
-
// 1. DNS-rebinding guard (Host header must be loopback on loopback bind)
|
|
215
|
-
// 2. Shared-token auth via Cookie (ws_token) or URL query param (?token=)
|
|
216
|
-
// 3. Loopback bootstrap: when requireToken=false (default) AND the server
|
|
217
|
-
// is bound to a loopback interface, browser clients bypass token auth
|
|
218
|
-
// entirely — the Origin header being http://localhost is sufficient.
|
|
219
|
-
//
|
|
220
|
-
// COOKIE PERSISTENCE AFTER TOKEN ROTATION:
|
|
221
|
-
// The HttpOnly ws_token cookie, once set by /ws-auth, survives a process
|
|
222
|
-
// restart. On the next process the expectedToken is a fresh random value,
|
|
223
|
-
// so the old cookie alone would fail the token check. However, on a
|
|
224
|
-
// loopback bind with requireToken=false (default), the loopback-bootstrap
|
|
225
|
-
// path at layer 3 accepts the connection before the cookie is ever
|
|
226
|
-
// compared — the old cookie is harmless because no token is needed.
|
|
227
|
-
//
|
|
228
|
-
// This means a token rotation (e.g. the operator sets a new accessToken
|
|
229
|
-
// in config) does NOT invalidate already-distributed cookies for loopback
|
|
230
|
-
// clients. The mitigating factors:
|
|
231
|
-
// - Loopback bind + requireToken=false already trusts any local process
|
|
232
|
-
// that can reach 127.0.0.1 — a trivial perimeter.
|
|
233
|
-
// - A LAN/0.0.0.0 bind with requireToken=true DOES enforce cookie
|
|
234
|
-
// matching against the new expectedToken on every reconnect.
|
|
235
|
-
// If stronger isolation is needed, set requireToken=true or rebind the
|
|
236
|
-
// server to a non-loopback interface with token enforcement.
|
|
237
|
-
authenticate: (ws, req) => {
|
|
238
|
-
const allowed = verifyWsClient({
|
|
239
|
-
origin: req.headers.origin,
|
|
240
|
-
url: req.url ?? "/",
|
|
241
|
-
hostHeader: req.headers.host,
|
|
242
|
-
remoteAddress: req.socket.remoteAddress,
|
|
243
|
-
cookieHeader: req.headers.cookie,
|
|
244
|
-
wsHost: deps.host,
|
|
245
|
-
expectedToken: deps.wsToken,
|
|
246
|
-
requireToken: deps.requireToken,
|
|
247
|
-
allowedHostnames: deps.publicHostnames,
|
|
248
|
-
allowBrowserUrlToken: Boolean(deps.publicWsUrl)
|
|
249
|
-
});
|
|
250
|
-
if (!allowed) ws.close(4003, "Forbidden");
|
|
251
|
-
return allowed;
|
|
252
|
-
},
|
|
253
|
-
createClient: (ws) => ({ ws, sessionId: deps.currentSessionId() }),
|
|
254
|
-
// Enrich parse-failure / unknown-type logs with per-connection identity
|
|
255
|
-
// so operators can identify which client sent a malformed frame. The
|
|
256
|
-
// `connectionId`/`sessionId` fields are read from the `client` argument
|
|
257
|
-
// directly inside `logRejection`; the CLI's `ConnectedClient` only has
|
|
258
|
-
// `sessionId` (no per-socket `connId`), so the verbose payload carries
|
|
259
|
-
// `sessionId` only. Server-wide identity (`agentId`/`projectRoot`) isn't
|
|
260
|
-
// in scope at this handler and is omitted.
|
|
261
|
-
verboseWsLogging: true,
|
|
262
|
-
registerClient: (ws) => {
|
|
263
|
-
for (const handler of clientHandlers) handler.addClient(ws);
|
|
264
|
-
},
|
|
265
|
-
decode: (raw) => decodeProtocolFrame(raw, "client"),
|
|
266
|
-
dispatch: deps.handleMessage,
|
|
267
|
-
send: deps.send,
|
|
268
|
-
sessionPayload: deps.sessionPayload,
|
|
269
|
-
rateLimitMax: deps.rateLimitMax,
|
|
270
|
-
// NOTE — a closed SOCKET is not a stopped run.
|
|
271
|
-
//
|
|
272
|
-
// This used to `abortControllers.delete(client.sessionId)` — a delete
|
|
273
|
-
// WITHOUT an abort, and only for the tab this connection last acted on.
|
|
274
|
-
// Both halves were wrong. The run kept going while `isRunActive` reported
|
|
275
|
-
// `false`, so `session.delete` would happily unlink the journal a live run
|
|
276
|
-
// was still appending to, and the next `user_message` claimed a second
|
|
277
|
-
// lock for the same session and hit `Agent.run() is already in progress`.
|
|
278
|
-
// The other three declared tabs were never touched at all.
|
|
279
|
-
//
|
|
280
|
-
// Doing nothing is the correct behaviour: a background run outlives the
|
|
281
|
-
// tab that started it (the same invariant `retireUndisplayedSessions`
|
|
282
|
-
// honours), and the run's own `finally` releases the lock when it unwinds.
|
|
283
|
-
// A page that comes back is told what is still running by
|
|
284
|
-
// `session.run_state`; a browser that never comes back drains its
|
|
285
|
-
// unanswerable permission prompts through the last-client drain in
|
|
286
|
-
// `connection-lifecycle.ts`, which lets a wedged run finish and release.
|
|
287
|
-
//
|
|
288
|
-
// So there is deliberately NO `onClose` here.
|
|
289
|
-
buildInitialPayload: async () => {
|
|
290
|
-
const payload = { ...await deps.buildSessionStartPayload({}, deps.needsSetup) };
|
|
291
|
-
try {
|
|
292
|
-
const open = deps.openSessionIds?.();
|
|
293
|
-
if (open) payload["openSessionIds"] = [.../* @__PURE__ */ new Set([...open, deps.currentSessionId()])];
|
|
294
|
-
} catch {
|
|
295
|
-
console.debug("[WebUI] Failed to list open sessions");
|
|
296
|
-
}
|
|
297
|
-
let replayEvents;
|
|
298
|
-
try {
|
|
299
|
-
const replay = await deps.loadReplay?.();
|
|
300
|
-
if (replay) {
|
|
301
|
-
Object.assign(payload, buildReplayPayload(replay));
|
|
302
|
-
replayEvents = replay.events;
|
|
490
|
+
// itself under (`hqSessionId`) — not for whatever else the browser
|
|
491
|
+
// has open. This used to abort every controller in the map and clear
|
|
492
|
+
// it, so a remote "interrupt" issued against the leader also killed
|
|
493
|
+
// the three other tabs' in-flight runs. Deleting the entries was
|
|
494
|
+
// wrong on its own terms too: the run's own `end()` owns removal, and
|
|
495
|
+
// clearing early makes `isRunActive` lie to every tab still running.
|
|
496
|
+
// Every open tab is its own session with its own abort controller, so
|
|
497
|
+
// the command's session is the one that gets stopped. Falling back to
|
|
498
|
+
// the boot session keeps a dashboard that sends no session — every one
|
|
499
|
+
// before this existed — behaving exactly as before.
|
|
500
|
+
interruptLeader: (sessionId) => {
|
|
501
|
+
const leaderId = sessionId ?? opts.session.id;
|
|
502
|
+
const controller = abortControllers.get(leaderId);
|
|
503
|
+
if (!controller) return false;
|
|
504
|
+
controller.abort();
|
|
505
|
+
try {
|
|
506
|
+
void Promise.resolve(opts.stopSessionFleet?.(leaderId)).catch(() => void 0);
|
|
507
|
+
} catch {
|
|
303
508
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
509
|
+
return true;
|
|
510
|
+
},
|
|
511
|
+
allowRunCommand: () => opts.hqAllowExec === true,
|
|
512
|
+
// A command naming a tab this process no longer holds is refused, not
|
|
513
|
+
// redirected onto the boot session: the operator picked a terminal,
|
|
514
|
+
// and steering a different one is worse than not steering at all.
|
|
515
|
+
ownsSession: (sessionId) => sessionId === opts.session.id || getSessionAgents()?.has(sessionId) === true
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
function startEmbeddedLiveStatusLogger(opts, clients, abortControllers, getSessionAgents, terminalLogView) {
|
|
520
|
+
return startWebUILiveStatusLogger({
|
|
521
|
+
events: opts.events,
|
|
522
|
+
dashboard: terminalLogView,
|
|
523
|
+
getSessionList: () => {
|
|
524
|
+
const ids = /* @__PURE__ */ new Set();
|
|
525
|
+
for (const client of clients.values()) {
|
|
526
|
+
if (client.sessionId) ids.add(client.sessionId);
|
|
527
|
+
for (const id of client.sessionIds ?? []) ids.add(id);
|
|
313
528
|
}
|
|
314
|
-
|
|
529
|
+
const currentId = opts.agent.ctx.session?.id ?? opts.session.id;
|
|
530
|
+
if (ids.size === 0 && currentId) ids.add(currentId);
|
|
531
|
+
return Array.from(ids).map((id) => {
|
|
532
|
+
const ctx = getSessionAgents()?.peek(id)?.ctx;
|
|
533
|
+
return {
|
|
534
|
+
id,
|
|
535
|
+
model: ctx?.model ?? opts.agent.ctx.model ?? "",
|
|
536
|
+
provider: ctx?.provider?.id ?? opts.agent.ctx.provider?.id ?? "",
|
|
537
|
+
isRunning: abortControllers.has(id)
|
|
538
|
+
};
|
|
539
|
+
});
|
|
315
540
|
}
|
|
316
541
|
});
|
|
317
542
|
}
|
|
@@ -319,7 +544,7 @@ function createConnectionHandler(deps) {
|
|
|
319
544
|
// src/webui-server/credential-watcher.ts
|
|
320
545
|
import { TOKENS } from "@wrongstack/core/kernel";
|
|
321
546
|
import { watchProviderConfig } from "@wrongstack/core/storage";
|
|
322
|
-
import { toErrorMessage } from "@wrongstack/core/utils/error";
|
|
547
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils/error";
|
|
323
548
|
import { makeProviderFromConfig } from "@wrongstack/providers";
|
|
324
549
|
|
|
325
550
|
// src/webui-server/provider-config.ts
|
|
@@ -474,7 +699,7 @@ function startWebuiCredentialWatcher({
|
|
|
474
699
|
console.log(`[WebUI] Provider credentials reloaded from config.json (${activeId})`);
|
|
475
700
|
} catch (err) {
|
|
476
701
|
console.warn(
|
|
477
|
-
`[WebUI] Credential hot-reload failed for ${activeId}: ${
|
|
702
|
+
`[WebUI] Credential hot-reload failed for ${activeId}: ${toErrorMessage2(err)}`
|
|
478
703
|
);
|
|
479
704
|
}
|
|
480
705
|
},
|
|
@@ -506,35 +731,6 @@ import {
|
|
|
506
731
|
WorktreeWebSocketHandler
|
|
507
732
|
} from "@wrongstack/webui-server";
|
|
508
733
|
|
|
509
|
-
// src/webui-server/logger-shim.ts
|
|
510
|
-
var structuredLine = (level, message) => JSON.stringify({
|
|
511
|
-
level,
|
|
512
|
-
event: "webui.goal",
|
|
513
|
-
message,
|
|
514
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
515
|
-
});
|
|
516
|
-
var consoleLogger = {
|
|
517
|
-
level: "debug",
|
|
518
|
-
error(msg, _ctx) {
|
|
519
|
-
console.error(structuredLine("error", msg));
|
|
520
|
-
},
|
|
521
|
-
warn(msg, _ctx) {
|
|
522
|
-
console.warn(structuredLine("warn", msg));
|
|
523
|
-
},
|
|
524
|
-
info(msg, _ctx) {
|
|
525
|
-
console.log(structuredLine("info", msg));
|
|
526
|
-
},
|
|
527
|
-
debug(msg, _ctx) {
|
|
528
|
-
console.debug(structuredLine("debug", msg));
|
|
529
|
-
},
|
|
530
|
-
trace(msg, _ctx) {
|
|
531
|
-
console.debug(structuredLine("trace", msg));
|
|
532
|
-
},
|
|
533
|
-
child(_bindings) {
|
|
534
|
-
return this;
|
|
535
|
-
}
|
|
536
|
-
};
|
|
537
|
-
|
|
538
734
|
// src/webui-server/node-pty-loader.ts
|
|
539
735
|
import { existsSync } from "node:fs";
|
|
540
736
|
import { createRequire } from "node:module";
|
|
@@ -602,7 +798,7 @@ function createNodePtyLoader(importMetaUrl, logger) {
|
|
|
602
798
|
|
|
603
799
|
// src/webui-server/domain-handlers.ts
|
|
604
800
|
function createWebuiDomainHandlers(opts, trustBoundary, kanbanRunMirror) {
|
|
605
|
-
const goalStoreDir = opts.projectRoot ?
|
|
801
|
+
const goalStoreDir = opts.projectRoot ? resolveWstackPaths({ projectRoot: opts.projectRoot }).projectAutophase : path3.join(os.tmpdir(), ".wrongstack", "goal");
|
|
606
802
|
const goalHandler = new GoalWebSocketHandler(
|
|
607
803
|
opts.agent,
|
|
608
804
|
opts.agent.ctx,
|
|
@@ -610,7 +806,8 @@ function createWebuiDomainHandlers(opts, trustBoundary, kanbanRunMirror) {
|
|
|
610
806
|
goalStoreDir,
|
|
611
807
|
opts.events,
|
|
612
808
|
opts.projectRoot,
|
|
613
|
-
kanbanRunMirror ? (graphId, state) => kanbanRunMirror.onGoalState(graphId, state) : void 0
|
|
809
|
+
kanbanRunMirror ? (graphId, state) => kanbanRunMirror.onGoalState(graphId, state) : void 0,
|
|
810
|
+
opts.sddSubagentFactory
|
|
614
811
|
);
|
|
615
812
|
const worktreeHandler = new WorktreeWebSocketHandler(opts.events, consoleLogger);
|
|
616
813
|
const loadNodePtyViaWebui = createNodePtyLoader(import.meta.url, consoleLogger);
|
|
@@ -837,14 +1034,6 @@ function createCliKanbanHostRoutes(deps) {
|
|
|
837
1034
|
};
|
|
838
1035
|
}
|
|
839
1036
|
|
|
840
|
-
// src/webui-server/lifecycle.ts
|
|
841
|
-
import {
|
|
842
|
-
announceWebuiReady,
|
|
843
|
-
createWebuiShutdown,
|
|
844
|
-
registerWebuiInstance,
|
|
845
|
-
registerWebuiSignalHandlers
|
|
846
|
-
} from "@wrongstack/webui-server";
|
|
847
|
-
|
|
848
1037
|
// src/webui-server/listen-helpers.ts
|
|
849
1038
|
import * as http from "node:http";
|
|
850
1039
|
import { listenWithRetry } from "@wrongstack/webui-server";
|
|
@@ -1008,6 +1197,7 @@ import {
|
|
|
1008
1197
|
import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
|
|
1009
1198
|
import { SkillInstaller } from "@wrongstack/core/skills";
|
|
1010
1199
|
import { PromptUsageStore } from "@wrongstack/core/storage";
|
|
1200
|
+
import { resolveTokenSavingTier } from "@wrongstack/core/types";
|
|
1011
1201
|
import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
|
|
1012
1202
|
import {
|
|
1013
1203
|
createChimeraRouteHandlers,
|
|
@@ -1142,6 +1332,16 @@ function createWebuiRouteContexts({
|
|
|
1142
1332
|
pendingConfirms,
|
|
1143
1333
|
configStore: opts.agent.container?.safeResolve?.(TOKENS2.ConfigStore),
|
|
1144
1334
|
systemPrompt: {
|
|
1335
|
+
previewContext: (sessionId) => {
|
|
1336
|
+
const target = sessionId ? getSessionAgent(sessionId).ctx : opts.agent.ctx;
|
|
1337
|
+
return {
|
|
1338
|
+
toolNames: target.tools.map((tool) => tool.name),
|
|
1339
|
+
tier: resolveTokenSavingTier(
|
|
1340
|
+
opts.appConfig?.features?.tokenSavingMode,
|
|
1341
|
+
target.provider?.capabilities?.maxContext
|
|
1342
|
+
)
|
|
1343
|
+
};
|
|
1344
|
+
},
|
|
1145
1345
|
paths: () => {
|
|
1146
1346
|
const wpaths = resolveWstackPaths3({ projectRoot: promptProjectRoot(), globalRoot });
|
|
1147
1347
|
return {
|
|
@@ -1291,6 +1491,7 @@ function createWebuiRouteContexts({
|
|
|
1291
1491
|
// src/webui-server/session-start-payload.ts
|
|
1292
1492
|
import * as path6 from "node:path";
|
|
1293
1493
|
import { DEFAULT_CONTEXT_WINDOW_MODE_ID } from "@wrongstack/core/types";
|
|
1494
|
+
import { catalogProviderIdFor } from "@wrongstack/providers";
|
|
1294
1495
|
import { protocolAdvertisement } from "@wrongstack/webui-protocol";
|
|
1295
1496
|
|
|
1296
1497
|
// src/webui-server/cost-helpers.ts
|
|
@@ -1312,7 +1513,11 @@ function createSessionStartPayloadBuilder(deps) {
|
|
|
1312
1513
|
const updateInfo = deps.updateInfo;
|
|
1313
1514
|
try {
|
|
1314
1515
|
if (deps.modelsRegistry) {
|
|
1315
|
-
const
|
|
1516
|
+
const catalogId = catalogProviderIdFor(
|
|
1517
|
+
ctx.provider.id,
|
|
1518
|
+
deps.appConfig?.providers?.[ctx.provider.id]?.type
|
|
1519
|
+
);
|
|
1520
|
+
const m = await deps.modelsRegistry.getModel(catalogId, ctx.model);
|
|
1316
1521
|
const registryMax = m?.capabilities.maxContext;
|
|
1317
1522
|
maxContext = registryMax ?? ctx.provider.capabilities?.maxContext ?? 0;
|
|
1318
1523
|
const rates = getCostRates(m);
|
|
@@ -1562,6 +1767,9 @@ async function runWebUI(opts) {
|
|
|
1562
1767
|
get session() {
|
|
1563
1768
|
return opts.session;
|
|
1564
1769
|
},
|
|
1770
|
+
get appConfig() {
|
|
1771
|
+
return opts.appConfig;
|
|
1772
|
+
},
|
|
1565
1773
|
// `peek`, never `get`: building a payload must not materialise an agent
|
|
1566
1774
|
// for a session id that arrived from a stale browser tab.
|
|
1567
1775
|
getSessionContext: (sessionId) => sessionAgentsRef?.peek(sessionId)?.ctx
|
|
@@ -1768,7 +1976,7 @@ async function runWebUI(opts) {
|
|
|
1768
1976
|
log: (m) => console.log(m)
|
|
1769
1977
|
};
|
|
1770
1978
|
const embeddedProviderOperations = createEmbeddedProviderOperations(wsHandlerCtx);
|
|
1771
|
-
|
|
1979
|
+
const credentialWatcherClose = startWebuiCredentialWatcher({
|
|
1772
1980
|
opts,
|
|
1773
1981
|
profileConfigPath,
|
|
1774
1982
|
broadcast,
|
|
@@ -1842,7 +2050,15 @@ async function runWebUI(opts) {
|
|
|
1842
2050
|
void sessionAgents.endAndClose(sessionId);
|
|
1843
2051
|
}
|
|
1844
2052
|
};
|
|
1845
|
-
const
|
|
2053
|
+
const refreshSessionHistory = async () => {
|
|
2054
|
+
const list = await opts.sessionStore?.list(200);
|
|
2055
|
+
if (!list) return;
|
|
2056
|
+
broadcastEveryone({
|
|
2057
|
+
type: "sessions.list",
|
|
2058
|
+
payload: { sessions: toSessionHistoryEntries(list, foregroundSession.id) }
|
|
2059
|
+
});
|
|
2060
|
+
};
|
|
2061
|
+
const emptySessionCleanup = opts.sessionStore ? scheduleOwnerlessEmptySessionCleanup({
|
|
1846
2062
|
getSessionStore: () => opts.sessionStore,
|
|
1847
2063
|
getActiveSessionId: () => foregroundSession.id,
|
|
1848
2064
|
getActiveSessionIds: () => collectDisplayedSessionIds({ getSession: () => foregroundSession, clients }),
|
|
@@ -1850,16 +2066,19 @@ async function runWebUI(opts) {
|
|
|
1850
2066
|
// written its first record yet looks exactly like a session nobody
|
|
1851
2067
|
// ever used.
|
|
1852
2068
|
hasParticipants: (sessionId) => abortControllers.has(sessionId),
|
|
1853
|
-
refreshSessions:
|
|
1854
|
-
const list = await opts.sessionStore?.list(200);
|
|
1855
|
-
if (!list) return;
|
|
1856
|
-
broadcastEveryone({
|
|
1857
|
-
type: "sessions.list",
|
|
1858
|
-
payload: { sessions: toSessionHistoryEntries(list, foregroundSession.id) }
|
|
1859
|
-
});
|
|
1860
|
-
},
|
|
2069
|
+
refreshSessions: refreshSessionHistory,
|
|
1861
2070
|
logger: consoleLogger
|
|
1862
2071
|
}) : null;
|
|
2072
|
+
const offSessionRenamed = opts.sessionStore ? opts.events.on("session.renamed", () => {
|
|
2073
|
+
void refreshSessionHistory().catch(() => void 0);
|
|
2074
|
+
}) : void 0;
|
|
2075
|
+
const stopEmptySessionCleanup = emptySessionCleanup && {
|
|
2076
|
+
runNow: emptySessionCleanup.runNow,
|
|
2077
|
+
dispose: async () => {
|
|
2078
|
+
offSessionRenamed?.();
|
|
2079
|
+
await emptySessionCleanup.dispose();
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
1863
2082
|
const routeContexts = createWebuiRouteContexts({
|
|
1864
2083
|
opts,
|
|
1865
2084
|
profileConfigPath,
|
|
@@ -1936,148 +2155,72 @@ async function runWebUI(opts) {
|
|
|
1936
2155
|
kanbanHostRoutes,
|
|
1937
2156
|
statusTracker: opts.statusTracker
|
|
1938
2157
|
});
|
|
1939
|
-
const stopped =
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
goalHandler,
|
|
1972
|
-
specsHandler,
|
|
1973
|
-
sddBoardHandler,
|
|
1974
|
-
sddWizardHandler,
|
|
1975
|
-
worktreeHandler,
|
|
1976
|
-
terminalHandler,
|
|
1977
|
-
rateLimitMax,
|
|
1978
|
-
send,
|
|
1979
|
-
sessionPayload,
|
|
1980
|
-
handleMessage,
|
|
1981
|
-
pendingConfirms,
|
|
1982
|
-
buildSessionStartPayload,
|
|
1983
|
-
loadReplay: async () => {
|
|
1984
|
-
const activeSession = opts.agent.ctx.session ?? opts.session;
|
|
1985
|
-
await activeSession.flush();
|
|
1986
|
-
if (opts.sessionStore) {
|
|
1987
|
-
const data = await opts.sessionStore.load(activeSession.id);
|
|
1988
|
-
return { messages: data.messages, events: data.events, usage: data.usage };
|
|
1989
|
-
}
|
|
1990
|
-
const usage = opts.agent.ctx.tokenCounter.total();
|
|
1991
|
-
return { messages: opts.agent.ctx.messages, usage };
|
|
1992
|
-
},
|
|
1993
|
-
loadAgentSessions: async (subagentIds) => await opts.agentTranscripts?.loadSessionsFromDisk(subagentIds) ?? [],
|
|
1994
|
-
// What this process is actually holding right now. The browser
|
|
1995
|
-
// reconciles its persisted tab strip against it, so a restarted server
|
|
1996
|
-
// is not dressed in the previous run's tabs.
|
|
1997
|
-
openSessionIds: () => sessionAgents.ids(),
|
|
1998
|
-
needsSetup: opts.needsSetup ?? false
|
|
1999
|
-
})
|
|
2000
|
-
);
|
|
2001
|
-
signalShutdown = createWebuiShutdown({
|
|
2002
|
-
abortInFlight: () => {
|
|
2003
|
-
void embeddedAutoHealDispose?.();
|
|
2004
|
-
for (const c of abortControllers.values()) c.abort();
|
|
2005
|
-
abortControllers.clear();
|
|
2006
|
-
},
|
|
2007
|
-
unsubscribeEvents: () => {
|
|
2008
|
-
flushAllStreamBuffers();
|
|
2009
|
-
for (const unsub of eventUnsubscribers) unsub();
|
|
2010
|
-
},
|
|
2011
|
-
stopOwnedChildren: async () => {
|
|
2012
|
-
try {
|
|
2013
|
-
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
2014
|
-
getProcessRegistry().killAll({ force: true, includeProtected: true });
|
|
2015
|
-
} catch (err) {
|
|
2016
|
-
console.debug(
|
|
2017
|
-
`[webui-server] process-registry killAll failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2018
|
-
);
|
|
2019
|
-
}
|
|
2020
|
-
if (opts.mcpRegistry) {
|
|
2021
|
-
try {
|
|
2022
|
-
await opts.mcpRegistry.stopAll();
|
|
2023
|
-
} catch (err) {
|
|
2024
|
-
console.debug(
|
|
2025
|
-
`[webui-server] mcpRegistry.stopAll failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2026
|
-
);
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
},
|
|
2030
|
-
disposeResources: async () => {
|
|
2031
|
-
releaseSessionSalvage();
|
|
2032
|
-
autoWakeHost?.dispose();
|
|
2033
|
-
await stopEmptySessionCleanup?.dispose();
|
|
2034
|
-
await sessionAgents.closeAll().catch(() => void 0);
|
|
2035
|
-
credentialWatcherClose?.();
|
|
2036
|
-
credentialWatcherClose = void 0;
|
|
2037
|
-
goalHandler.dispose();
|
|
2038
|
-
sddBoardHandler.dispose();
|
|
2039
|
-
worktreeHandler.dispose();
|
|
2040
|
-
terminalHandler.dispose();
|
|
2041
|
-
kanbanRunMirror?.dispose();
|
|
2042
|
-
kanbanSupervisor?.dispose();
|
|
2043
|
-
void stopKanbanSupervisorMemoryStats?.();
|
|
2044
|
-
await embeddedAutoHealDispose?.();
|
|
2045
|
-
embeddedAutoHealDispose = null;
|
|
2046
|
-
unregisterWebuiClient();
|
|
2158
|
+
const { stopped } = runWebuiServerLifecycle({
|
|
2159
|
+
host,
|
|
2160
|
+
httpPort,
|
|
2161
|
+
setupEvents,
|
|
2162
|
+
opts,
|
|
2163
|
+
wsPort,
|
|
2164
|
+
accessUrl,
|
|
2165
|
+
wsToken,
|
|
2166
|
+
webuiInstanceRegistered,
|
|
2167
|
+
wss,
|
|
2168
|
+
httpServer,
|
|
2169
|
+
requireToken,
|
|
2170
|
+
publicHostnames,
|
|
2171
|
+
publicWsUrl,
|
|
2172
|
+
clients,
|
|
2173
|
+
currentSessionId,
|
|
2174
|
+
goalHandler,
|
|
2175
|
+
specsHandler,
|
|
2176
|
+
sddBoardHandler,
|
|
2177
|
+
sddWizardHandler,
|
|
2178
|
+
worktreeHandler,
|
|
2179
|
+
terminalHandler,
|
|
2180
|
+
rateLimitMax,
|
|
2181
|
+
send,
|
|
2182
|
+
sessionPayload,
|
|
2183
|
+
handleMessage,
|
|
2184
|
+
pendingConfirms,
|
|
2185
|
+
buildSessionStartPayload,
|
|
2186
|
+
sessionAgents,
|
|
2187
|
+
lifecycleState: {
|
|
2188
|
+
get signalShutdown() {
|
|
2189
|
+
return signalShutdown;
|
|
2047
2190
|
},
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
clients.clear();
|
|
2191
|
+
set signalShutdown(value) {
|
|
2192
|
+
signalShutdown = value;
|
|
2051
2193
|
},
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
httpServer?.server.close();
|
|
2194
|
+
get embeddedAutoHealDispose() {
|
|
2195
|
+
return embeddedAutoHealDispose;
|
|
2055
2196
|
},
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
registryBaseDir,
|
|
2059
|
-
onStopped: () => {
|
|
2060
|
-
stopLiveStatusLogger();
|
|
2061
|
-
const muted = terminalLogView.mutedCount;
|
|
2062
|
-
terminalLogView.stop();
|
|
2063
|
-
if (muted > 0) {
|
|
2064
|
-
console.log(
|
|
2065
|
-
`[WebUI] ${muted} progress line(s) kept out of this terminal \u2014 set WEBUI_LOGS=1 to stream them.`
|
|
2066
|
-
);
|
|
2067
|
-
}
|
|
2068
|
-
opts.onExit?.();
|
|
2069
|
-
resolve();
|
|
2197
|
+
set embeddedAutoHealDispose(value) {
|
|
2198
|
+
embeddedAutoHealDispose = value;
|
|
2070
2199
|
}
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2200
|
+
},
|
|
2201
|
+
abortControllers,
|
|
2202
|
+
flushAllStreamBuffers,
|
|
2203
|
+
eventUnsubscribers,
|
|
2204
|
+
releaseSessionSalvage,
|
|
2205
|
+
autoWakeHost,
|
|
2206
|
+
stopEmptySessionCleanup,
|
|
2207
|
+
credentialWatcherClose,
|
|
2208
|
+
kanbanRunMirror,
|
|
2209
|
+
kanbanSupervisor,
|
|
2210
|
+
stopKanbanSupervisorMemoryStats,
|
|
2211
|
+
unregisterWebuiClient,
|
|
2212
|
+
ipv6LoopbackServer,
|
|
2213
|
+
registryBaseDir,
|
|
2214
|
+
stopLiveStatusLogger,
|
|
2215
|
+
terminalLogView
|
|
2073
2216
|
});
|
|
2074
2217
|
function send(ws, msg) {
|
|
2075
2218
|
sendSerialized(ws, JSON.stringify(stampDispatchSession(msg)));
|
|
2076
2219
|
}
|
|
2077
2220
|
function broadcast(msg, targetSessionId) {
|
|
2078
|
-
const data = JSON.stringify(msg);
|
|
2079
2221
|
const payload = msg.payload;
|
|
2080
2222
|
const sessionId = targetSessionId ?? (payload && typeof payload === "object" && typeof payload.sessionId === "string" ? payload.sessionId : void 0);
|
|
2223
|
+
const data = sessionId ? webuiSessionFrameLog().sequence(sessionId, msg) : JSON.stringify(msg);
|
|
2081
2224
|
for (const [ws, client] of clients) {
|
|
2082
2225
|
if (clientWantsSession(client, sessionId)) sendSerialized(ws, data);
|
|
2083
2226
|
}
|
|
@@ -2094,4 +2237,4 @@ async function runWebUI(opts) {
|
|
|
2094
2237
|
export {
|
|
2095
2238
|
runWebUI
|
|
2096
2239
|
};
|
|
2097
|
-
//# sourceMappingURL=webui-server-
|
|
2240
|
+
//# sourceMappingURL=webui-server-YTHLRFD2.js.map
|