@rynx-ai/server 0.1.10 → 0.1.11-beta.10
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/control-api.d.ts +17 -52
- package/dist/control-api.js +755 -341
- package/dist/control-web/assets/{highlighted-body-OFNGDK62-DgFZpTNw.js → highlighted-body-OFNGDK62-IdclXfSt.js} +1 -1
- package/dist/control-web/assets/index-5n6D-i-c.css +32 -0
- package/dist/control-web/assets/index-DhsO7WjD.js +709 -0
- package/dist/control-web/assets/{mermaid-GHXKKRXX-BDikE3W1.js → mermaid-GHXKKRXX-BqMPrbIZ.js} +3 -3
- package/dist/control-web/index.html +2 -2
- package/dist/control-web-dist.d.ts +1 -1
- package/dist/control-web-dist.js +4 -2
- package/dist/machine-session-service.d.ts +135 -27
- package/dist/machine-session-service.js +410 -58
- package/dist/remote-runtime-dispatcher.d.ts +12 -2
- package/dist/remote-runtime-dispatcher.js +56 -0
- package/dist/remote-runtime-session-projection.js +1 -0
- package/dist/server.d.ts +59 -29
- package/dist/server.js +158 -143
- package/dist/session-browser-service.d.ts +8 -0
- package/dist/session-browser-service.js +37 -20
- package/dist/session-portal.d.ts +42 -0
- package/dist/session-portal.js +746 -0
- package/dist/session-runtime-index.d.ts +7 -0
- package/dist/session-runtime-index.js +42 -6
- package/dist/session-terminal-host.d.ts +6 -0
- package/dist/session-terminal-host.js +56 -41
- package/dist/terminal-ws.js +8 -6
- package/package.json +7 -7
- package/dist/channel-manager.d.ts +0 -60
- package/dist/channel-manager.js +0 -106
- package/dist/control-web/assets/index-CnVtOmLv.css +0 -32
- package/dist/control-web/assets/index-Dlywgy58.js +0 -661
package/dist/server.js
CHANGED
|
@@ -3,7 +3,6 @@ import Koa from "koa";
|
|
|
3
3
|
import Router from "@koa/router";
|
|
4
4
|
import { loadConfig, listAgentSpecs, loadAgentSpec, resolveSessionExecution, resolveRuntimeModel, ConversationRuntime, InMemorySessionBus, persistSessionEvent, } from "@rynx-ai/core";
|
|
5
5
|
import { RunnerManager, FileCodexSessionStore, resolveCodexSessionStorePath, } from "@rynx-ai/runtime";
|
|
6
|
-
import { ChannelManager } from "./channel-manager.js";
|
|
7
6
|
import { createControlRouter, } from "./control-api.js";
|
|
8
7
|
import { attachEmulatorTouchWs } from "./emulator-touch-ws.js";
|
|
9
8
|
import { attachTerminalWs } from "./terminal-ws.js";
|
|
@@ -11,10 +10,11 @@ import { attachBrowserSurfaceWs } from "./browser-surface-ws.js";
|
|
|
11
10
|
import { attachEmulatorSurfaceWs } from "./emulator-surface-ws.js";
|
|
12
11
|
import { attachDesktopBrowserHostServer, } from "./desktop-browser-host.js";
|
|
13
12
|
import { resolveControlWebDist, sendSpaFile } from "./control-web-dist.js";
|
|
13
|
+
import { startSessionPortal, } from "./session-portal.js";
|
|
14
|
+
export { createSessionPortalApp, startSessionPortal } from "./session-portal.js";
|
|
14
15
|
import { SessionRuntimeIndex } from "./session-runtime-index.js";
|
|
15
16
|
import { createRunnerSessionTerminalHost, } from "./session-terminal-host.js";
|
|
16
17
|
import { runtimeWebAccessPolicyForBindHost, } from "./runtime-web-auth.js";
|
|
17
|
-
export { ChannelManager } from "./channel-manager.js";
|
|
18
18
|
export { MachineSessionService, MachineSessionServiceFailure, MachineSessionServiceInputError, synthesizeSessionTitle, } from "./machine-session-service.js";
|
|
19
19
|
export { SessionBrowserHostError, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceUnsupportedError, } from "./session-browser-service.js";
|
|
20
20
|
export { SessionEmulatorBindingConflictError, SessionEmulatorService, SessionEmulatorServiceError, } from "./session-emulator-service.js";
|
|
@@ -26,6 +26,7 @@ export { attachDirectRuntimeBrowserSurfaceServer, DirectRuntimeBrowserSurfaceHos
|
|
|
26
26
|
export { attachDirectRuntimeBrowserInspectServer, DirectRuntimeBrowserInspectHostError, } from "./direct-runtime-browser-inspect-server.js";
|
|
27
27
|
export { RemoteRuntimeDispatcher } from "./remote-runtime-dispatcher.js";
|
|
28
28
|
export { createRunnerSessionTerminalHost, SessionTerminalOpenError, SessionTerminalStreamError, } from "./session-terminal-host.js";
|
|
29
|
+
export { listRuntimeModels } from "@rynx-ai/runtime";
|
|
29
30
|
/** Preserve one session's mirror order without serializing unrelated sessions. */
|
|
30
31
|
export class SessionMirrorQueue {
|
|
31
32
|
persist;
|
|
@@ -60,18 +61,24 @@ export function createSessionRuntimeServices(input) {
|
|
|
60
61
|
...(input.sessionContextProvider
|
|
61
62
|
? { sessionContextProvider: input.sessionContextProvider }
|
|
62
63
|
: {}),
|
|
64
|
+
...(input.admissionOpen ? { admissionOpen: input.admissionOpen } : {}),
|
|
65
|
+
...(input.admissionReserve ? { admissionReserve: input.admissionReserve } : {}),
|
|
63
66
|
});
|
|
64
67
|
const sessionBus = new InMemorySessionBus();
|
|
65
68
|
const sessionRuntimeIndex = new SessionRuntimeIndex();
|
|
69
|
+
const sessionMirrorQueue = new SessionMirrorQueue((sessionId, event) => persistSessionEvent(sessionId, event, { sessionLog: input.sessionLog, sessionBus }));
|
|
70
|
+
const mirrorSessionEvent = (sessionId, event) => {
|
|
71
|
+
sessionRuntimeIndex.observe(sessionId, event);
|
|
72
|
+
return sessionMirrorQueue.enqueue(sessionId, event);
|
|
73
|
+
};
|
|
66
74
|
const conversationRuntime = new ConversationRuntime({
|
|
67
75
|
config: input.config,
|
|
68
76
|
executor: runnerManager,
|
|
69
77
|
sessionBus,
|
|
78
|
+
sessionEventSink: mirrorSessionEvent,
|
|
70
79
|
});
|
|
71
|
-
const sessionMirrorQueue = new SessionMirrorQueue((sessionId, event) => persistSessionEvent(sessionId, event, { sessionLog: input.sessionLog, sessionBus }));
|
|
72
80
|
runnerManager.onMirror((sessionId, event) => {
|
|
73
|
-
|
|
74
|
-
void sessionMirrorQueue.enqueue(sessionId, event).catch((error) => {
|
|
81
|
+
void mirrorSessionEvent(sessionId, event).catch((error) => {
|
|
75
82
|
input.onMirrorError?.(error, sessionId);
|
|
76
83
|
});
|
|
77
84
|
});
|
|
@@ -81,6 +88,7 @@ export function createSessionRuntimeServices(input) {
|
|
|
81
88
|
sessionBus,
|
|
82
89
|
sessionRuntimeIndex,
|
|
83
90
|
conversationRuntime,
|
|
91
|
+
publishSessionEvent: mirrorSessionEvent,
|
|
84
92
|
};
|
|
85
93
|
}
|
|
86
94
|
/**
|
|
@@ -131,7 +139,7 @@ export function createApp({ config = loadConfig(), control } = {}) {
|
|
|
131
139
|
}
|
|
132
140
|
return app;
|
|
133
141
|
}
|
|
134
|
-
export async function startServer({ config = loadConfig(),
|
|
142
|
+
export async function startServer({ config = loadConfig(), control, sessionLog, sessionRegistry, pluginRuntime, daemonManagement, remoteRuntime, remoteRuntimeAdmin, runtimeTargetControl, runtimeConnectionResolver, localSessionResources, localMachineSessions, sessionEmulators, sessionRuntimeServices, sessionLifecycle, sessionTerminals, runtimeLocalBrowser, providerClis, desktopBrowserHost, sessionPortalAuthorization, } = {}) {
|
|
135
143
|
const runtimeWebAccess = runtimeWebAccessPolicyForBindHost(config.HOST);
|
|
136
144
|
const desktopBrowserManagementToken = desktopBrowserHost
|
|
137
145
|
? requireDaemonManagementToken(daemonManagement)
|
|
@@ -148,12 +156,52 @@ export async function startServer({ config = loadConfig(), channelInstances, loa
|
|
|
148
156
|
createRunnerSessionTerminalHost({
|
|
149
157
|
runnerManager,
|
|
150
158
|
resolveLiveSession: async (sessionId) => {
|
|
151
|
-
const session =
|
|
159
|
+
const session = sessionRegistry?.get(sessionId);
|
|
152
160
|
if (!session || !runnerManager.hasLiveSession(sessionId))
|
|
153
161
|
return undefined;
|
|
154
|
-
return { cwd: session.cwd };
|
|
162
|
+
return { cwd: session.workspace.cwd };
|
|
155
163
|
},
|
|
156
164
|
});
|
|
165
|
+
const controlShutdown = new AbortController();
|
|
166
|
+
const controlSurface = control
|
|
167
|
+
? {
|
|
168
|
+
deps: control,
|
|
169
|
+
runtime: conversationRuntime,
|
|
170
|
+
sessionBus,
|
|
171
|
+
sessionLog,
|
|
172
|
+
runnerManager,
|
|
173
|
+
sessionStore,
|
|
174
|
+
pluginRuntimeStatus: pluginRuntime
|
|
175
|
+
? () => pluginRuntime.statusSnapshot()
|
|
176
|
+
: undefined,
|
|
177
|
+
daemonManagement,
|
|
178
|
+
remoteRuntime,
|
|
179
|
+
remoteRuntimeAdmin,
|
|
180
|
+
runtimeTargetControl,
|
|
181
|
+
runtimeConnectionResolver,
|
|
182
|
+
localSessionResources,
|
|
183
|
+
localMachineSessions,
|
|
184
|
+
sessionEmulators,
|
|
185
|
+
shutdownSignal: controlShutdown.signal,
|
|
186
|
+
sessionRuntimeIndex,
|
|
187
|
+
sessionLifecycle,
|
|
188
|
+
runtimeLocalBrowser,
|
|
189
|
+
providerClis,
|
|
190
|
+
}
|
|
191
|
+
: undefined;
|
|
192
|
+
const sessionPortalConfig = config.RYNX_SESSION_PORTAL_PORT !== undefined
|
|
193
|
+
? (() => {
|
|
194
|
+
if (runtimeWebAccess.mode !== "loopback") {
|
|
195
|
+
throw new Error("Session Portal requires the ordinary Control listener to bind loopback only");
|
|
196
|
+
}
|
|
197
|
+
const controlWebDist = resolveControlWebDist();
|
|
198
|
+
if (!sessionPortalAuthorization || !controlSurface || !controlWebDist) {
|
|
199
|
+
throw new Error("Session Portal requires authorization, Control services, and the native Control Web build");
|
|
200
|
+
}
|
|
201
|
+
return { authorization: sessionPortalAuthorization, controlWebDist };
|
|
202
|
+
})()
|
|
203
|
+
: undefined;
|
|
204
|
+
let sessionPortalServer;
|
|
157
205
|
if (pluginRuntime) {
|
|
158
206
|
if (!sessionRegistry)
|
|
159
207
|
throw new Error("plugin runtime requires a machine session registry");
|
|
@@ -175,165 +223,132 @@ export async function startServer({ config = loadConfig(), channelInstances, loa
|
|
|
175
223
|
}),
|
|
176
224
|
},
|
|
177
225
|
interruptSession: (sessionId) => runnerManager.interruptLiveSession(sessionId),
|
|
226
|
+
terminateSession: (sessionId) => runnerManager.terminateLiveSession(sessionId),
|
|
178
227
|
resolveSessionInteraction: (sessionId, interactionId, resolution) => runnerManager.resolveInteraction(sessionId, interactionId, resolution),
|
|
228
|
+
...(sessionPortalConfig
|
|
229
|
+
? {
|
|
230
|
+
sessionPortal: {
|
|
231
|
+
scheme: "http",
|
|
232
|
+
bindHost: config.RYNX_SESSION_PORTAL_HOST,
|
|
233
|
+
port: config.RYNX_SESSION_PORTAL_PORT,
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
: {}),
|
|
179
237
|
});
|
|
180
238
|
}
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
239
|
+
// Provider TUI `/clear`·`/fork` uses the same publication boundary as an
|
|
240
|
+
// explicit Rynx fork. The manager exposes the transferred terminal only after
|
|
241
|
+
// this resolves, so no target can appear with missing history or metadata.
|
|
184
242
|
runnerManager.onRotate((r) => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
? { provider: source.provider }
|
|
193
|
-
: {}),
|
|
194
|
-
...(r.model ? { model: r.model } : {}),
|
|
195
|
-
createdAt: new Date().toISOString(),
|
|
243
|
+
if (!localMachineSessions) {
|
|
244
|
+
throw new Error("native Session rotation requires the machine Session service");
|
|
245
|
+
}
|
|
246
|
+
return localMachineSessions.recordNativeRotation({
|
|
247
|
+
sourceSessionId: r.from,
|
|
248
|
+
targetSessionId: r.to,
|
|
249
|
+
kind: r.kind,
|
|
196
250
|
});
|
|
197
251
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
sessionBus,
|
|
221
|
-
sessionLog,
|
|
222
|
-
runnerManager,
|
|
223
|
-
sessionStore,
|
|
224
|
-
pluginRuntimeStatus: pluginRuntime
|
|
225
|
-
? () => pluginRuntime.statusSnapshot()
|
|
226
|
-
: undefined,
|
|
227
|
-
pluginRuntimeReload: pluginRuntime
|
|
228
|
-
? (pluginId) => pluginRuntime.reloadPluginRuntime(pluginId)
|
|
229
|
-
: undefined,
|
|
230
|
-
pluginRuntimeEnsure: pluginRuntime
|
|
231
|
-
? (pluginId) => pluginRuntime.ensurePluginRuntime(pluginId)
|
|
232
|
-
: undefined,
|
|
233
|
-
daemonManagement,
|
|
234
|
-
remoteRuntime,
|
|
235
|
-
remoteRuntimeAdmin,
|
|
236
|
-
runtimeTargetControl,
|
|
237
|
-
runtimeConnectionResolver,
|
|
238
|
-
localSessionResources,
|
|
239
|
-
sessionEmulators,
|
|
240
|
-
shutdownSignal: controlShutdown.signal,
|
|
241
|
-
sessionRuntimeIndex,
|
|
242
|
-
sessionLifecycle,
|
|
243
|
-
runtimeLocalBrowser,
|
|
244
|
-
providerClis,
|
|
245
|
-
}
|
|
246
|
-
: undefined,
|
|
247
|
-
});
|
|
248
|
-
return new Promise((resolve) => {
|
|
249
|
-
const server = app.listen(config.PORT, config.HOST, () => {
|
|
250
|
-
console.log(JSON.stringify({
|
|
251
|
-
level: config.LOG_LEVEL,
|
|
252
|
-
msg: "Harness agent server listening",
|
|
253
|
-
host: config.HOST,
|
|
254
|
-
port: config.PORT,
|
|
255
|
-
runtime: config.AGENT_RUNTIME,
|
|
256
|
-
model: resolveRuntimeModel(config, config.AGENT_RUNTIME),
|
|
257
|
-
}));
|
|
258
|
-
const startup = (async () => {
|
|
259
|
-
await pluginRuntime?.startPluginRuntimes();
|
|
260
|
-
await manager.startAll();
|
|
261
|
-
})();
|
|
262
|
-
void startup.catch((error) => {
|
|
263
|
-
console.error(JSON.stringify({
|
|
264
|
-
level: "error",
|
|
265
|
-
type: "plugin-runtime",
|
|
266
|
-
event: "startup_failed",
|
|
267
|
-
error: error instanceof Error ? error.message : String(error),
|
|
268
|
-
}));
|
|
269
|
-
});
|
|
270
|
-
// Live WS bridges share this http.Server (no second port).
|
|
271
|
-
const webSocketServers = [
|
|
272
|
-
attachTerminalWs(server, {
|
|
273
|
-
runtimeConnectionResolver,
|
|
274
|
-
legacyLocalTerminalHost,
|
|
275
|
-
runtimeWebAccess,
|
|
276
|
-
}),
|
|
277
|
-
attachBrowserSurfaceWs(server, { runtimeConnectionResolver, runtimeWebAccess }),
|
|
278
|
-
attachEmulatorSurfaceWs(server, { runtimeConnectionResolver, runtimeWebAccess }),
|
|
279
|
-
];
|
|
280
|
-
if (desktopBrowserHost && desktopBrowserManagementToken) {
|
|
281
|
-
webSocketServers.push(attachDesktopBrowserHostServer(server, {
|
|
282
|
-
...desktopBrowserHost,
|
|
283
|
-
managementToken: desktopBrowserManagementToken,
|
|
284
|
-
}));
|
|
285
|
-
}
|
|
286
|
-
if (control?.emulator) {
|
|
287
|
-
webSocketServers.push(attachEmulatorTouchWs(server, { emulator: control.emulator }));
|
|
288
|
-
}
|
|
289
|
-
resolve(bindServerLifecycle(server, {
|
|
290
|
-
fence: () => {
|
|
291
|
-
controlShutdown.abort(new Error("Rynx server is shutting down"));
|
|
292
|
-
},
|
|
293
|
-
closeUpgrades: async () => {
|
|
294
|
-
await closeWebSocketServers(webSocketServers);
|
|
295
|
-
},
|
|
296
|
-
cleanup: async () => {
|
|
297
|
-
await runCleanupSteps([
|
|
298
|
-
() => startup.catch(() => undefined),
|
|
299
|
-
() => manager.stopAll(),
|
|
300
|
-
() => Promise.resolve(daemonManagement?.pluginManagement?.dispose?.()),
|
|
301
|
-
() => pluginRuntime?.dispose() ?? Promise.resolve(),
|
|
302
|
-
() => runnerManager.stop(),
|
|
303
|
-
]);
|
|
304
|
-
},
|
|
305
|
-
}));
|
|
252
|
+
const app = createApp({
|
|
253
|
+
config,
|
|
254
|
+
control: controlSurface,
|
|
255
|
+
});
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
let startupWebSocketServers = [];
|
|
258
|
+
const rejectBeforeListening = (error) => reject(error);
|
|
259
|
+
const server = app.listen(config.PORT, config.HOST, () => {
|
|
260
|
+
server.off("error", rejectBeforeListening);
|
|
261
|
+
void finishStartup(server).catch(async (error) => {
|
|
262
|
+
controlShutdown.abort(asError(error));
|
|
263
|
+
sessionPortalServer?.stopAccepting();
|
|
264
|
+
server.closeAllConnections?.();
|
|
265
|
+
await Promise.allSettled([
|
|
266
|
+
closeWebSocketServers(startupWebSocketServers),
|
|
267
|
+
sessionPortalServer?.shutdown() ?? Promise.resolve(),
|
|
268
|
+
new Promise((done) => server.close(() => done())),
|
|
269
|
+
Promise.resolve(daemonManagement?.pluginManagement?.dispose?.()),
|
|
270
|
+
Promise.resolve(pluginRuntime?.dispose()),
|
|
271
|
+
runnerManager.stop(),
|
|
272
|
+
]);
|
|
273
|
+
reject(error);
|
|
306
274
|
});
|
|
307
275
|
});
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const app = createApp();
|
|
311
|
-
return new Promise((resolve) => {
|
|
312
|
-
const server = app.listen(config.PORT, config.HOST, () => {
|
|
276
|
+
server.once("error", rejectBeforeListening);
|
|
277
|
+
const finishStartup = async (server) => {
|
|
313
278
|
console.log(JSON.stringify({
|
|
314
279
|
level: config.LOG_LEVEL,
|
|
315
280
|
msg: "Harness agent server listening",
|
|
316
281
|
host: config.HOST,
|
|
317
282
|
port: config.PORT,
|
|
318
|
-
runtime: config.
|
|
319
|
-
model: resolveRuntimeModel(config, config.
|
|
283
|
+
runtime: config.DEFAULT_RUNTIME,
|
|
284
|
+
model: resolveRuntimeModel(config, config.DEFAULT_RUNTIME),
|
|
320
285
|
}));
|
|
321
|
-
const webSocketServers = [
|
|
286
|
+
const webSocketServers = [
|
|
287
|
+
attachTerminalWs(server, {
|
|
288
|
+
runtimeConnectionResolver,
|
|
289
|
+
legacyLocalTerminalHost,
|
|
290
|
+
runtimeWebAccess,
|
|
291
|
+
}),
|
|
292
|
+
attachBrowserSurfaceWs(server, { runtimeConnectionResolver, runtimeWebAccess }),
|
|
293
|
+
attachEmulatorSurfaceWs(server, { runtimeConnectionResolver, runtimeWebAccess }),
|
|
294
|
+
];
|
|
322
295
|
if (desktopBrowserHost && desktopBrowserManagementToken) {
|
|
323
296
|
webSocketServers.push(attachDesktopBrowserHostServer(server, {
|
|
324
297
|
...desktopBrowserHost,
|
|
325
298
|
managementToken: desktopBrowserManagementToken,
|
|
326
299
|
}));
|
|
327
300
|
}
|
|
301
|
+
if (control?.emulator) {
|
|
302
|
+
webSocketServers.push(attachEmulatorTouchWs(server, { emulator: control.emulator }));
|
|
303
|
+
}
|
|
304
|
+
startupWebSocketServers = webSocketServers;
|
|
305
|
+
if (sessionPortalConfig) {
|
|
306
|
+
sessionPortalServer = await startSessionPortal({
|
|
307
|
+
host: config.RYNX_SESSION_PORTAL_HOST,
|
|
308
|
+
port: config.RYNX_SESSION_PORTAL_PORT,
|
|
309
|
+
scheme: "http",
|
|
310
|
+
authorization: sessionPortalConfig.authorization,
|
|
311
|
+
controlOrigin: listeningHttpOrigin(server),
|
|
312
|
+
controlWebDist: sessionPortalConfig.controlWebDist,
|
|
313
|
+
shutdownSignal: controlShutdown.signal,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const startup = pluginRuntime?.startPluginRuntimes() ?? Promise.resolve();
|
|
317
|
+
void startup.catch((error) => {
|
|
318
|
+
console.error(JSON.stringify({
|
|
319
|
+
level: "error",
|
|
320
|
+
type: "plugin-runtime",
|
|
321
|
+
event: "startup_failed",
|
|
322
|
+
error: error instanceof Error ? error.message : String(error),
|
|
323
|
+
}));
|
|
324
|
+
});
|
|
328
325
|
resolve(bindServerLifecycle(server, {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
326
|
+
fence: () => {
|
|
327
|
+
controlShutdown.abort(new Error("Rynx server is shutting down"));
|
|
328
|
+
sessionPortalServer?.stopAccepting();
|
|
329
|
+
},
|
|
330
|
+
closeUpgrades: () => Promise.all([
|
|
331
|
+
closeWebSocketServers(webSocketServers),
|
|
332
|
+
sessionPortalServer?.shutdown() ?? Promise.resolve(),
|
|
333
|
+
]).then(() => undefined),
|
|
334
|
+
cleanup: async () => runCleanupSteps([
|
|
335
|
+
() => startup.catch(() => undefined),
|
|
336
|
+
() => Promise.resolve(daemonManagement?.pluginManagement?.dispose?.()),
|
|
337
|
+
() => pluginRuntime?.dispose() ?? Promise.resolve(),
|
|
338
|
+
() => runnerManager.stop(),
|
|
339
|
+
]),
|
|
333
340
|
}));
|
|
334
|
-
}
|
|
341
|
+
};
|
|
335
342
|
});
|
|
336
343
|
}
|
|
344
|
+
function listeningHttpOrigin(server) {
|
|
345
|
+
const address = server.address();
|
|
346
|
+
if (!address || typeof address === "string") {
|
|
347
|
+
throw new Error("Control listener did not expose an HTTP address");
|
|
348
|
+
}
|
|
349
|
+
const host = address.address.includes(":") ? `[${address.address}]` : address.address;
|
|
350
|
+
return `http://${host}:${address.port}`;
|
|
351
|
+
}
|
|
337
352
|
function requireDaemonManagementToken(host) {
|
|
338
353
|
const token = host?.managementToken;
|
|
339
354
|
const bytes = typeof token === "string" ? Buffer.byteLength(token, "utf8") : 0;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AdmissionReservation } from "@rynx-ai/core";
|
|
1
2
|
import { type RuntimeBrowserCloseParams, type RuntimeBrowserCloseResult, type RuntimeBrowserExecutionBackend, type RuntimeBrowserOpenParams, type RuntimeBrowserPageCreateParams, type RuntimeBrowserPageMutationParams, type RuntimeBrowserPageNavigateParams, type RuntimeBrowserState, type RuntimeBrowserStateGetParams } from "@rynx-ai/protocol/runtime-browser";
|
|
2
3
|
import type { RuntimeBrowserSurfaceFormat, RuntimeBrowserSurfaceInputEvent } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
3
4
|
import { type RuntimeBrowserEndpointDescriptor } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
@@ -155,6 +156,11 @@ export interface SessionBrowserServiceOptions {
|
|
|
155
156
|
pageIdFactory?: () => string;
|
|
156
157
|
/** Injectable boot nonce; production randomization fences stale pre-restart mutations. */
|
|
157
158
|
bootGenerationSeed?: number;
|
|
159
|
+
/**
|
|
160
|
+
* Synchronous daemon admission fence. A successful reservation is held until
|
|
161
|
+
* Browser open has either failed or registered the physical Host handle.
|
|
162
|
+
*/
|
|
163
|
+
admissionReserve?: () => AdmissionReservation | undefined;
|
|
158
164
|
}
|
|
159
165
|
export type SessionBrowserHostErrorCode = "unavailable" | "page_not_found" | "capacity" | "outcome_unknown";
|
|
160
166
|
/** Expected failure reported by a Browser Host implementation. */
|
|
@@ -181,6 +187,7 @@ export declare class SessionBrowserService {
|
|
|
181
187
|
private readonly observers;
|
|
182
188
|
private readonly pageIdFactory;
|
|
183
189
|
private readonly bootGenerationSeed;
|
|
190
|
+
private readonly admissionReserve?;
|
|
184
191
|
private shuttingDown;
|
|
185
192
|
private closeAllPromise?;
|
|
186
193
|
constructor(ports: SessionBrowserServicePorts, options?: SessionBrowserServiceOptions);
|
|
@@ -225,6 +232,7 @@ export declare class SessionBrowserService {
|
|
|
225
232
|
closeAll(): Promise<void>;
|
|
226
233
|
private closeEveryRecord;
|
|
227
234
|
private createBrowser;
|
|
235
|
+
private reserveOpenAdmission;
|
|
228
236
|
private closeUnavailableForRecreate;
|
|
229
237
|
private pageMutation;
|
|
230
238
|
private pageMutationParsed;
|
|
@@ -42,12 +42,14 @@ export class SessionBrowserService {
|
|
|
42
42
|
observers = new Set();
|
|
43
43
|
pageIdFactory;
|
|
44
44
|
bootGenerationSeed;
|
|
45
|
+
admissionReserve;
|
|
45
46
|
shuttingDown = false;
|
|
46
47
|
closeAllPromise;
|
|
47
48
|
constructor(ports, options = {}) {
|
|
48
49
|
this.ports = ports;
|
|
49
50
|
this.pageIdFactory = options.pageIdFactory ?? (() => `page_${randomUUID()}`);
|
|
50
51
|
this.bootGenerationSeed = options.bootGenerationSeed ?? randomBootGenerationSeed();
|
|
52
|
+
this.admissionReserve = options.admissionReserve;
|
|
51
53
|
if (!Number.isSafeInteger(this.bootGenerationSeed) ||
|
|
52
54
|
this.bootGenerationSeed < 0 ||
|
|
53
55
|
this.bootGenerationSeed >= Number.MAX_SAFE_INTEGER) {
|
|
@@ -224,29 +226,35 @@ export class SessionBrowserService {
|
|
|
224
226
|
});
|
|
225
227
|
}
|
|
226
228
|
async open(input) {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
await this.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (existing?.status === "ready") {
|
|
236
|
-
try {
|
|
237
|
-
await this.reconcileForRead(existing);
|
|
229
|
+
const admission = this.reserveOpenAdmission();
|
|
230
|
+
try {
|
|
231
|
+
const params = parseInput(() => parseRuntimeBrowserOpenParams(input), "Browser open request is invalid");
|
|
232
|
+
return await this.exclusive(params.sessionId, async () => {
|
|
233
|
+
this.ensureRunning();
|
|
234
|
+
await this.ensureSessionExists(params.sessionId);
|
|
235
|
+
const existing = this.records.get(params.sessionId);
|
|
236
|
+
if (existing?.status === "opening") {
|
|
238
237
|
return this.project(existing);
|
|
239
238
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
239
|
+
if (existing?.status === "ready") {
|
|
240
|
+
try {
|
|
241
|
+
await this.reconcileForRead(existing);
|
|
242
|
+
return this.project(existing);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// An App Host may have gone away immediately before this open. The
|
|
246
|
+
// failed reconciliation marks the old generation unavailable, so an
|
|
247
|
+
// idempotent open can recreate it on the current preferred Host.
|
|
248
|
+
}
|
|
244
249
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
+
if (existing)
|
|
251
|
+
await this.closeUnavailableForRecreate(existing);
|
|
252
|
+
return this.createBrowser(params);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
admission?.release();
|
|
257
|
+
}
|
|
250
258
|
}
|
|
251
259
|
async close(input) {
|
|
252
260
|
const params = parseInput(() => parseRuntimeBrowserCloseParams(input), "Browser close request is invalid");
|
|
@@ -438,6 +446,15 @@ export class SessionBrowserService {
|
|
|
438
446
|
throw new SessionBrowserServiceError("host_failure", "Browser Host state could not be initialized", { cause: error });
|
|
439
447
|
}
|
|
440
448
|
}
|
|
449
|
+
reserveOpenAdmission() {
|
|
450
|
+
if (!this.admissionReserve)
|
|
451
|
+
return undefined;
|
|
452
|
+
const admission = this.admissionReserve();
|
|
453
|
+
if (!admission) {
|
|
454
|
+
throw new SessionBrowserServiceError("shutting_down", "Browser service is shutting down");
|
|
455
|
+
}
|
|
456
|
+
return admission;
|
|
457
|
+
}
|
|
441
458
|
async closeUnavailableForRecreate(record) {
|
|
442
459
|
record.status = "closing";
|
|
443
460
|
try {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type IncomingMessage, type Server as HttpServer } from "node:http";
|
|
2
|
+
import type { Duplex } from "node:stream";
|
|
3
|
+
import Koa from "koa";
|
|
4
|
+
export declare const SESSION_PORTAL_HEADER = "x-rynx-session-portal";
|
|
5
|
+
export declare const SESSION_PORTAL_QUERY = "portal";
|
|
6
|
+
export interface SessionPortalAuthorization {
|
|
7
|
+
portalId: string;
|
|
8
|
+
pluginId: string;
|
|
9
|
+
scope: "plugin_sessions" | "single_session";
|
|
10
|
+
access: "read_only" | "read_write";
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SessionPortalAuthorizationPorts {
|
|
15
|
+
redeem(ticket: string): (SessionPortalAuthorization & {
|
|
16
|
+
secret: string;
|
|
17
|
+
}) | undefined;
|
|
18
|
+
authorize(portalId: string, secret: string): SessionPortalAuthorization | undefined;
|
|
19
|
+
ownsSession(pluginId: string, sessionId: string): boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface SessionPortalOptions {
|
|
22
|
+
host: string;
|
|
23
|
+
port: number;
|
|
24
|
+
scheme: "http" | "https";
|
|
25
|
+
authorization: SessionPortalAuthorizationPorts;
|
|
26
|
+
controlOrigin: string;
|
|
27
|
+
controlWebDist: string;
|
|
28
|
+
shutdownSignal?: AbortSignal;
|
|
29
|
+
}
|
|
30
|
+
export interface SessionPortalServer {
|
|
31
|
+
server: HttpServer;
|
|
32
|
+
stopAccepting(): void;
|
|
33
|
+
shutdown(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/** Session Portal is a narrow adapter in front of the one Control service. It
|
|
36
|
+
* serves the native SPA, validates Portal grants, and forwards only authorized
|
|
37
|
+
* Session HTTP calls to Control; it owns no Session or Runtime handlers. */
|
|
38
|
+
export declare function createSessionPortalApp(options: SessionPortalOptions): Koa;
|
|
39
|
+
export declare function startSessionPortal(options: SessionPortalOptions): Promise<SessionPortalServer>;
|
|
40
|
+
export declare function authorizeSessionPortalWebSocket(request: IncomingMessage, ports: SessionPortalAuthorizationPorts, runtimeSelector: string, sessionId: string): SessionPortalAuthorization | undefined;
|
|
41
|
+
export declare function rejectSessionPortalUpgrade(socket: Duplex): void;
|
|
42
|
+
export declare function scheduleSessionPortalExpiry(expiresAt: string, onExpire: () => void): () => void;
|