@rynx-ai/server 0.1.11-beta.5 → 0.2.0-beta.2

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.
@@ -7,7 +7,7 @@
7
7
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
8
  <link rel="icon" type="image/png" href="/favicon.png" />
9
9
  <title>rynx · control</title>
10
- <script type="module" crossorigin src="/assets/index-D56JQ9oi.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-DhsO7WjD.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-5n6D-i-c.css">
12
12
  </head>
13
13
  <body>
@@ -3,4 +3,4 @@ import type { Context } from "koa";
3
3
  export declare function resolveControlWebDist(): string | null;
4
4
  /** Serve a static file from the SPA dir; missing non-asset paths fall back to
5
5
  * index.html (client-side routing), missing assets return 404. */
6
- export declare function sendSpaFile(ctx: Context, distDir: string, basePath?: string): Promise<void>;
6
+ export declare function sendSpaFile(ctx: Context, distDir: string, basePath?: string, transformIndex?: (html: string) => string): Promise<void>;
@@ -37,7 +37,7 @@ export function resolveControlWebDist() {
37
37
  }
38
38
  /** Serve a static file from the SPA dir; missing non-asset paths fall back to
39
39
  * index.html (client-side routing), missing assets return 404. */
40
- export async function sendSpaFile(ctx, distDir, basePath = "/") {
40
+ export async function sendSpaFile(ctx, distDir, basePath = "/", transformIndex) {
41
41
  const path = basePath !== "/" && ctx.path.startsWith(basePath)
42
42
  ? ctx.path.slice(basePath.length) || "/"
43
43
  : ctx.path;
@@ -64,5 +64,7 @@ export async function sendSpaFile(ctx, distDir, basePath = "/") {
64
64
  }
65
65
  }
66
66
  ctx.type = CONTENT_TYPES[ext] ?? "application/octet-stream";
67
- ctx.body = data;
67
+ ctx.body = ext === ".html" && transformIndex
68
+ ? transformIndex(data.toString("utf8"))
69
+ : data;
68
70
  }
package/dist/server.d.ts CHANGED
@@ -18,6 +18,9 @@ import { type ControlPlaneDeps, type LocalSessionResourceHost, type SessionResou
18
18
  import type { MachineSessionService } from "./machine-session-service.js";
19
19
  import type { DaemonRuntimeHost } from "./remote-runtime.js";
20
20
  import { type AttachDesktopBrowserHostServerOptions } from "./desktop-browser-host.js";
21
+ import { type SessionPortalAuthorizationPorts } from "./session-portal.js";
22
+ export { createSessionPortalApp, startSessionPortal } from "./session-portal.js";
23
+ export type { SessionPortalAuthorization, SessionPortalAuthorizationPorts, SessionPortalOptions, SessionPortalServer, } from "./session-portal.js";
21
24
  import { SessionRuntimeIndex } from "./session-runtime-index.js";
22
25
  import type { SessionEmulatorService } from "./session-emulator-service.js";
23
26
  import { type SessionTerminalHost } from "./session-terminal-host.js";
@@ -132,6 +135,9 @@ export interface StartServerOptions {
132
135
  providerClis?: ProviderCliManagementHost;
133
136
  /** Private resident-app Browser Host socket, authenticated by daemonManagement. */
134
137
  desktopBrowserHost?: Omit<AttachDesktopBrowserHostServerOptions, "managementToken">;
138
+ /** Hashed ticket/grant authority for the native Session Portal. Required
139
+ * when RYNX_SESSION_PORTAL_PORT enables that listener. */
140
+ sessionPortalAuthorization?: SessionPortalAuthorizationPorts;
135
141
  }
136
142
  export interface SessionRuntimeServices {
137
143
  sessionStore: CodexSessionStore;
@@ -163,6 +169,11 @@ export interface PluginRuntimeHostServices {
163
169
  * closed, but cannot provision Sessions or begin Turns. */
164
170
  admissionOpen?: () => boolean;
165
171
  admissionReserve?: () => AdmissionReservation | undefined;
172
+ sessionPortal?: {
173
+ scheme: "http" | "https";
174
+ bindHost: string;
175
+ port: number;
176
+ };
166
177
  }
167
178
  export interface PluginRuntimeLifecycle {
168
179
  bindHostServices(services: PluginRuntimeHostServices): void;
@@ -434,4 +445,4 @@ export declare function createSessionRuntimeServices(input: {
434
445
  * no agent request / response / SSE endpoints are exposed here.
435
446
  */
436
447
  export declare function createApp({ config, control }?: CreateAppOptions): Koa<Koa.DefaultState, Koa.DefaultContext>;
437
- export declare function startServer({ config, control, sessionLog, sessionRegistry, pluginRuntime, daemonManagement, remoteRuntime, remoteRuntimeAdmin, runtimeTargetControl, runtimeConnectionResolver, localSessionResources, localMachineSessions, sessionEmulators, sessionRuntimeServices, sessionLifecycle, sessionTerminals, runtimeLocalBrowser, providerClis, desktopBrowserHost, }?: StartServerOptions): Promise<RynxServer>;
448
+ export declare function startServer({ config, control, sessionLog, sessionRegistry, pluginRuntime, daemonManagement, remoteRuntime, remoteRuntimeAdmin, runtimeTargetControl, runtimeConnectionResolver, localSessionResources, localMachineSessions, sessionEmulators, sessionRuntimeServices, sessionLifecycle, sessionTerminals, runtimeLocalBrowser, providerClis, desktopBrowserHost, sessionPortalAuthorization, }?: StartServerOptions): Promise<RynxServer>;
package/dist/server.js CHANGED
@@ -10,6 +10,8 @@ import { attachBrowserSurfaceWs } from "./browser-surface-ws.js";
10
10
  import { attachEmulatorSurfaceWs } from "./emulator-surface-ws.js";
11
11
  import { attachDesktopBrowserHostServer, } from "./desktop-browser-host.js";
12
12
  import { resolveControlWebDist, sendSpaFile } from "./control-web-dist.js";
13
+ import { startSessionPortal, } from "./session-portal.js";
14
+ export { createSessionPortalApp, startSessionPortal } from "./session-portal.js";
13
15
  import { SessionRuntimeIndex } from "./session-runtime-index.js";
14
16
  import { createRunnerSessionTerminalHost, } from "./session-terminal-host.js";
15
17
  import { runtimeWebAccessPolicyForBindHost, } from "./runtime-web-auth.js";
@@ -137,7 +139,7 @@ export function createApp({ config = loadConfig(), control } = {}) {
137
139
  }
138
140
  return app;
139
141
  }
140
- export async function startServer({ config = loadConfig(), control, sessionLog, sessionRegistry, pluginRuntime, daemonManagement, remoteRuntime, remoteRuntimeAdmin, runtimeTargetControl, runtimeConnectionResolver, localSessionResources, localMachineSessions, sessionEmulators, sessionRuntimeServices, sessionLifecycle, sessionTerminals, runtimeLocalBrowser, providerClis, desktopBrowserHost, } = {}) {
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, } = {}) {
141
143
  const runtimeWebAccess = runtimeWebAccessPolicyForBindHost(config.HOST);
142
144
  const desktopBrowserManagementToken = desktopBrowserHost
143
145
  ? requireDaemonManagementToken(daemonManagement)
@@ -160,6 +162,46 @@ export async function startServer({ config = loadConfig(), control, sessionLog,
160
162
  return { cwd: session.workspace.cwd };
161
163
  },
162
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;
163
205
  if (pluginRuntime) {
164
206
  if (!sessionRegistry)
165
207
  throw new Error("plugin runtime requires a machine session registry");
@@ -183,6 +225,15 @@ export async function startServer({ config = loadConfig(), control, sessionLog,
183
225
  interruptSession: (sessionId) => runnerManager.interruptLiveSession(sessionId),
184
226
  terminateSession: (sessionId) => runnerManager.terminateLiveSession(sessionId),
185
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
+ : {}),
186
237
  });
187
238
  }
188
239
  // Provider TUI `/clear`·`/fork` uses the same publication boundary as an
@@ -198,38 +249,32 @@ export async function startServer({ config = loadConfig(), control, sessionLog,
198
249
  kind: r.kind,
199
250
  });
200
251
  });
201
- const controlShutdown = new AbortController();
202
252
  const app = createApp({
203
253
  config,
204
- control: control
205
- ? {
206
- deps: control,
207
- runtime: conversationRuntime,
208
- sessionBus,
209
- sessionLog,
210
- runnerManager,
211
- sessionStore,
212
- pluginRuntimeStatus: pluginRuntime
213
- ? () => pluginRuntime.statusSnapshot()
214
- : undefined,
215
- daemonManagement,
216
- remoteRuntime,
217
- remoteRuntimeAdmin,
218
- runtimeTargetControl,
219
- runtimeConnectionResolver,
220
- localSessionResources,
221
- localMachineSessions,
222
- sessionEmulators,
223
- shutdownSignal: controlShutdown.signal,
224
- sessionRuntimeIndex,
225
- sessionLifecycle,
226
- runtimeLocalBrowser,
227
- providerClis,
228
- }
229
- : undefined,
254
+ control: controlSurface,
230
255
  });
231
- return new Promise((resolve) => {
256
+ return new Promise((resolve, reject) => {
257
+ let startupWebSocketServers = [];
258
+ const rejectBeforeListening = (error) => reject(error);
232
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);
274
+ });
275
+ });
276
+ server.once("error", rejectBeforeListening);
277
+ const finishStartup = async (server) => {
233
278
  console.log(JSON.stringify({
234
279
  level: config.LOG_LEVEL,
235
280
  msg: "Harness agent server listening",
@@ -238,15 +283,6 @@ export async function startServer({ config = loadConfig(), control, sessionLog,
238
283
  runtime: config.DEFAULT_RUNTIME,
239
284
  model: resolveRuntimeModel(config, config.DEFAULT_RUNTIME),
240
285
  }));
241
- const startup = pluginRuntime?.startPluginRuntimes() ?? Promise.resolve();
242
- void startup.catch((error) => {
243
- console.error(JSON.stringify({
244
- level: "error",
245
- type: "plugin-runtime",
246
- event: "startup_failed",
247
- error: error instanceof Error ? error.message : String(error),
248
- }));
249
- });
250
286
  const webSocketServers = [
251
287
  attachTerminalWs(server, {
252
288
  runtimeConnectionResolver,
@@ -265,9 +301,36 @@ export async function startServer({ config = loadConfig(), control, sessionLog,
265
301
  if (control?.emulator) {
266
302
  webSocketServers.push(attachEmulatorTouchWs(server, { emulator: control.emulator }));
267
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
+ });
268
325
  resolve(bindServerLifecycle(server, {
269
- fence: () => controlShutdown.abort(new Error("Rynx server is shutting down")),
270
- closeUpgrades: () => closeWebSocketServers(webSocketServers),
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),
271
334
  cleanup: async () => runCleanupSteps([
272
335
  () => startup.catch(() => undefined),
273
336
  () => Promise.resolve(daemonManagement?.pluginManagement?.dispose?.()),
@@ -275,9 +338,17 @@ export async function startServer({ config = loadConfig(), control, sessionLog,
275
338
  () => runnerManager.stop(),
276
339
  ]),
277
340
  }));
278
- });
341
+ };
279
342
  });
280
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
+ }
281
352
  function requireDaemonManagementToken(host) {
282
353
  const token = host?.managementToken;
283
354
  const bytes = typeof token === "string" ? Buffer.byteLength(token, "utf8") : 0;
@@ -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;