@rynx-ai/server 0.1.11-beta.4 → 0.1.11-beta.6
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 +8 -1
- package/dist/control-api.js +140 -3
- package/dist/control-web/assets/{highlighted-body-OFNGDK62-DWWsUtHF.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-C1fxAgiw.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 +12 -3
- package/dist/machine-session-service.js +82 -5
- package/dist/remote-runtime-dispatcher.d.ts +1 -1
- package/dist/remote-runtime-dispatcher.js +4 -0
- package/dist/server.d.ts +12 -1
- package/dist/server.js +112 -41
- package/dist/session-portal.d.ts +42 -0
- package/dist/session-portal.js +746 -0
- package/dist/terminal-ws.js +8 -6
- package/package.json +7 -7
- package/dist/control-web/assets/index-BnX7NWJX.js +0 -689
- package/dist/control-web/assets/index-CUoWRX2i.css +0 -32
|
@@ -7,8 +7,8 @@
|
|
|
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-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-DhsO7WjD.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-5n6D-i-c.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="root"></div>
|
|
@@ -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>;
|
package/dist/control-web-dist.js
CHANGED
|
@@ -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 =
|
|
67
|
+
ctx.body = ext === ".html" && transformIndex
|
|
68
|
+
? transformIndex(data.toString("utf8"))
|
|
69
|
+
: data;
|
|
68
70
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type AdmissionReservation, type AgentRuntimeId, type ConversationRuntime, type ResolvedExecutionSnapshot, type SessionWorkspaceSnapshot, type SessionProviderId, type MachineSessionRecord, type ReasoningEffort, type RuntimeUserInput, type SessionBus, type SessionLogStore, type SessionRegistry } from "@rynx-ai/core";
|
|
2
2
|
import type { SequencedSessionEvent, SessionEvent, SessionInteractionResolution, SessionItem, UserContentPart } from "@rynx-ai/protocol";
|
|
3
3
|
import type { RemoteRuntimeSessionResourceDeleteParams, RemoteRuntimeSessionResourceDeleteResult, RemoteRuntimeSessionResourceGetParams, RemoteRuntimeSessionResourceGetResult, RemoteRuntimeSessionResourcePolicyResult, RemoteRuntimeSessionResourceReadParams, RemoteRuntimeSessionResourceReadResult, RemoteRuntimeSessionResourceUploadBeginParams, RemoteRuntimeSessionResourceUploadBeginResult, RemoteRuntimeSessionResourceUploadChunkParams, RemoteRuntimeSessionResourceUploadChunkResult, RemoteRuntimeSessionResourceUploadCommitParams, RemoteRuntimeSessionResourceUploadCommitResult } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
4
|
-
import type { ControlAgentSummary, ControlSessionInteractionResolveDisposition, ControlSessionRuntimeSnapshot } from "@rynx-ai/protocol/control";
|
|
5
|
-
import { type RemoteRuntimeSessionInterruptResult, type RemoteRuntimeSessionLaunchOptionsListResult, type RemoteRuntimeSessionListParams, type RemoteRuntimeSessionListResult, type RemoteRuntimeSessionSnapshotParams } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
4
|
+
import type { ControlAgentModelOption, ControlAgentSummary, ControlSessionInteractionResolveDisposition, ControlSessionRuntimeSnapshot, SessionPermissionPreset } from "@rynx-ai/protocol/control";
|
|
5
|
+
import { type RemoteRuntimeSessionInterruptResult, type RemoteRuntimeSessionExecutionGetResult, type RemoteRuntimeSessionExecutionUpdateParams, type RemoteRuntimeSessionExecutionUpdateResult, type RemoteRuntimeSessionLaunchOptionsListResult, type RemoteRuntimeSessionListParams, type RemoteRuntimeSessionListResult, type RemoteRuntimeSessionSnapshotParams } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
6
6
|
import type { CodexRuntimeStatus, InjectOutcome } from "@rynx-ai/runtime";
|
|
7
7
|
export interface MachineSessionRuntimeStatePort {
|
|
8
8
|
snapshot(sessionId: string): ControlSessionRuntimeSnapshot;
|
|
@@ -130,7 +130,7 @@ export interface MachineSessionForkStorePort {
|
|
|
130
130
|
markTargetDeleted(sessionId: string): void | Promise<void>;
|
|
131
131
|
}
|
|
132
132
|
export interface MachineSessionServicePorts {
|
|
133
|
-
registry: Pick<SessionRegistry, "create" | "get" | "list" | "setTitle" | "remove">;
|
|
133
|
+
registry: Pick<SessionRegistry, "create" | "get" | "list" | "setTitle" | "setExecution" | "remove">;
|
|
134
134
|
log: Pick<SessionLogStore, "list" | "listSessions" | "snapshot" | "deleteSession">;
|
|
135
135
|
events: Pick<SessionBus, "subscribe"> & Partial<Pick<SessionBus, "close">>;
|
|
136
136
|
/** Canonical persistence + fan-out for user turns accepted before a native
|
|
@@ -156,8 +156,10 @@ export interface MachineSessionServicePorts {
|
|
|
156
156
|
provider?: SessionProviderId;
|
|
157
157
|
model?: string;
|
|
158
158
|
reasoningEffort?: ReasoningEffort;
|
|
159
|
+
permissionPreset?: SessionPermissionPreset;
|
|
159
160
|
cwd: string;
|
|
160
161
|
}): Promise<ResolvedExecutionSnapshot>;
|
|
162
|
+
listModels?(provider: SessionProviderId): Promise<ControlAgentModelOption[]>;
|
|
161
163
|
runner: MachineSessionRunnerPort;
|
|
162
164
|
};
|
|
163
165
|
/** Resolve a temporary Project selection, or create a Session-only fallback directory. */
|
|
@@ -206,6 +208,7 @@ export type MachineSessionInterruptResult = RemoteRuntimeSessionInterruptResult;
|
|
|
206
208
|
interface MachineSessionCreateOptions {
|
|
207
209
|
model?: string;
|
|
208
210
|
reasoningEffort?: ReasoningEffort;
|
|
211
|
+
permissionPreset?: SessionPermissionPreset;
|
|
209
212
|
title?: string;
|
|
210
213
|
projectId?: string;
|
|
211
214
|
}
|
|
@@ -306,6 +309,12 @@ export declare class MachineSessionService {
|
|
|
306
309
|
}>;
|
|
307
310
|
/** Target-owned Provider readiness plus the existing Agent preset catalog. */
|
|
308
311
|
launchOptions(): Promise<RemoteRuntimeSessionLaunchOptionsListResult>;
|
|
312
|
+
/** Read the effective model defaults used for the next turn. */
|
|
313
|
+
executionSettings(sessionIdInput: string): Promise<RemoteRuntimeSessionExecutionGetResult>;
|
|
314
|
+
/** Replace only the model defaults of an idle Session. The runner is closed
|
|
315
|
+
* so every Provider resumes its native context with the new settings on the
|
|
316
|
+
* next turn. */
|
|
317
|
+
updateExecutionSettings(input: RemoteRuntimeSessionExecutionUpdateParams): Promise<RemoteRuntimeSessionExecutionUpdateResult>;
|
|
309
318
|
/** Create target-owned Session identity from one Agent preset or direct Provider. */
|
|
310
319
|
create(input: MachineSessionCreateInput): Promise<MachineSessionCreateResult>;
|
|
311
320
|
private createAdmitted;
|
|
@@ -159,11 +159,18 @@ export class MachineSessionService {
|
|
|
159
159
|
const statusCache = new Map();
|
|
160
160
|
const { agents } = await this.agentOptions();
|
|
161
161
|
const [providers, launchAgents] = await Promise.all([
|
|
162
|
-
Promise.all(SESSION_PROVIDER_IDS.map(async (provider) =>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
Promise.all(SESSION_PROVIDER_IDS.map(async (provider) => {
|
|
163
|
+
const execution = this.ports.execution;
|
|
164
|
+
const models = execution?.listModels
|
|
165
|
+
? await execution.listModels(provider).catch(() => [])
|
|
166
|
+
: undefined;
|
|
167
|
+
return {
|
|
168
|
+
id: provider,
|
|
169
|
+
name: getRuntimeProfile(provider).displayName,
|
|
170
|
+
...await this.runtimeReadiness(provider, statusCache),
|
|
171
|
+
...(models === undefined ? {} : { models }),
|
|
172
|
+
};
|
|
173
|
+
})),
|
|
167
174
|
Promise.all(agents.map(async (agent) => {
|
|
168
175
|
const execution = this.ports.execution;
|
|
169
176
|
if (!execution) {
|
|
@@ -192,6 +199,59 @@ export class MachineSessionService {
|
|
|
192
199
|
]);
|
|
193
200
|
return { providers, agents: launchAgents };
|
|
194
201
|
}
|
|
202
|
+
/** Read the effective model defaults used for the next turn. */
|
|
203
|
+
async executionSettings(sessionIdInput) {
|
|
204
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
205
|
+
const meta = this.ports.registry.get(sessionId);
|
|
206
|
+
if (!meta) {
|
|
207
|
+
throw new MachineSessionServiceFailure("not_found", "session not found");
|
|
208
|
+
}
|
|
209
|
+
return executionSettingsResult(meta.execution);
|
|
210
|
+
}
|
|
211
|
+
/** Replace only the model defaults of an idle Session. The runner is closed
|
|
212
|
+
* so every Provider resumes its native context with the new settings on the
|
|
213
|
+
* next turn. */
|
|
214
|
+
async updateExecutionSettings(input) {
|
|
215
|
+
const sessionId = validOpaqueToken(input.sessionId, "sessionId", MAX_SESSION_ID_CHARS);
|
|
216
|
+
const modelId = validRequiredText(input.model, "model");
|
|
217
|
+
const meta = this.ports.registry.get(sessionId);
|
|
218
|
+
if (!meta) {
|
|
219
|
+
throw new MachineSessionServiceFailure("not_found", "session not found");
|
|
220
|
+
}
|
|
221
|
+
await this.assertNotForkReserved(sessionId);
|
|
222
|
+
const runtime = this.ports.runtimeState.snapshot(sessionId);
|
|
223
|
+
const pendingMessage = await this.ports.pendingMessages?.get(sessionId);
|
|
224
|
+
if (runtime.status !== "idle" ||
|
|
225
|
+
runtime.activeResponseIds.length > 0 ||
|
|
226
|
+
runtime.pendingInteractions.length > 0 ||
|
|
227
|
+
pendingMessage) {
|
|
228
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session must be idle before changing model settings");
|
|
229
|
+
}
|
|
230
|
+
const executionPort = this.requireExecution();
|
|
231
|
+
if (!executionPort.listModels) {
|
|
232
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Runtime model catalog is unavailable");
|
|
233
|
+
}
|
|
234
|
+
const models = await executionPort.listModels(meta.execution.provider);
|
|
235
|
+
const model = models.find((candidate) => candidate.id === modelId);
|
|
236
|
+
if (!model) {
|
|
237
|
+
throw new MachineSessionServiceInputError(`model is not available for ${meta.execution.provider}`);
|
|
238
|
+
}
|
|
239
|
+
const reasoningEffort = input.reasoningEffort === null
|
|
240
|
+
? null
|
|
241
|
+
: validRequiredText(input.reasoningEffort, "reasoningEffort");
|
|
242
|
+
if (reasoningEffort !== null &&
|
|
243
|
+
!model.reasoningOptions.some((option) => option.value === reasoningEffort)) {
|
|
244
|
+
throw new MachineSessionServiceInputError(`reasoningEffort is not available for model ${model.id}`);
|
|
245
|
+
}
|
|
246
|
+
const nextExecution = {
|
|
247
|
+
...structuredClone(meta.execution),
|
|
248
|
+
model: model.id,
|
|
249
|
+
reasoningEffort,
|
|
250
|
+
};
|
|
251
|
+
this.ports.registry.setExecution(sessionId, nextExecution);
|
|
252
|
+
executionPort.runner.stopRunner(sessionId);
|
|
253
|
+
return executionSettingsResult(nextExecution);
|
|
254
|
+
}
|
|
195
255
|
/** Create target-owned Session identity from one Agent preset or direct Provider. */
|
|
196
256
|
async create(input) {
|
|
197
257
|
return this.withAdmission(() => this.createAdmitted(input));
|
|
@@ -223,6 +283,7 @@ export class MachineSessionService {
|
|
|
223
283
|
...target,
|
|
224
284
|
...(input.model === undefined ? {} : { model: input.model }),
|
|
225
285
|
...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
|
|
286
|
+
...(input.permissionPreset === undefined ? {} : { permissionPreset: input.permissionPreset }),
|
|
226
287
|
cwd: workspace.cwd,
|
|
227
288
|
});
|
|
228
289
|
const sessionId = newSessionId();
|
|
@@ -997,6 +1058,13 @@ function sessionSummary(id, meta, log, runtimeState, pendingState) {
|
|
|
997
1058
|
updatedAt: validTimestamp(updatedAt, "updatedAt"),
|
|
998
1059
|
};
|
|
999
1060
|
}
|
|
1061
|
+
function executionSettingsResult(execution) {
|
|
1062
|
+
return {
|
|
1063
|
+
provider: execution.provider,
|
|
1064
|
+
model: execution.model,
|
|
1065
|
+
reasoningEffort: execution.reasoningEffort,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1000
1068
|
function compareMetaRecency(left, right) {
|
|
1001
1069
|
const leftTime = validTimestamp(left.updatedAt ?? left.createdAt, "updatedAt");
|
|
1002
1070
|
const rightTime = validTimestamp(right.updatedAt ?? right.createdAt, "updatedAt");
|
|
@@ -1091,6 +1159,15 @@ function validOpaqueToken(value, field, maximum) {
|
|
|
1091
1159
|
}
|
|
1092
1160
|
return value;
|
|
1093
1161
|
}
|
|
1162
|
+
function validRequiredText(value, field) {
|
|
1163
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.length > 512) {
|
|
1164
|
+
throw new MachineSessionServiceInputError(`${field} must be a non-empty string of at most 512 characters`);
|
|
1165
|
+
}
|
|
1166
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
|
1167
|
+
throw new MachineSessionServiceInputError(`${field} contains invalid control characters`);
|
|
1168
|
+
}
|
|
1169
|
+
return value.trim();
|
|
1170
|
+
}
|
|
1094
1171
|
function validTimestamp(value, field) {
|
|
1095
1172
|
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
1096
1173
|
throw new MachineSessionServiceInputError(`${field} must be a valid timestamp`);
|
|
@@ -34,7 +34,7 @@ export interface RemoteRuntimeProjectHost {
|
|
|
34
34
|
update(projectId: string, input: RemoteRuntimeProjectCreateParams): RemoteRuntimeProject | undefined | Promise<RemoteRuntimeProject | undefined>;
|
|
35
35
|
remove(projectId: string): boolean | Promise<boolean>;
|
|
36
36
|
}
|
|
37
|
-
export type RemoteRuntimeSessionHost = Pick<MachineSessionService, "list" | "snapshot" | "watch" | "interrupt" | "agentOptions" | "launchOptions" | "create" | "fork" | "sendMessage" | "resourcePolicy" | "beginResourceUpload" | "writeResourceUploadChunk" | "commitResourceUpload" | "getResource" | "readResource" | "deleteResource" | "enqueueMessage" | "startTerminal" | "delete" | "resolveInteraction">;
|
|
37
|
+
export type RemoteRuntimeSessionHost = Pick<MachineSessionService, "list" | "snapshot" | "watch" | "interrupt" | "agentOptions" | "launchOptions" | "executionSettings" | "updateExecutionSettings" | "create" | "fork" | "sendMessage" | "resourcePolicy" | "beginResourceUpload" | "writeResourceUploadChunk" | "commitResourceUpload" | "getResource" | "readResource" | "deleteResource" | "enqueueMessage" | "startTerminal" | "delete" | "resolveInteraction">;
|
|
38
38
|
export type RemoteRuntimeBrowserHost = Pick<SessionBrowserService, "getState" | "open" | "close" | "createPage" | "closePage" | "activatePage" | "navigatePage" | "goBack" | "goForward" | "reload">;
|
|
39
39
|
export type RemoteRuntimeSessionEmulatorHost = Pick<SessionEmulatorService, "getState" | "listDevices" | "attach" | "detach" | "tap" | "type" | "button" | "rotate" | "launch" | "gesture" | "releaseDevice" | "stopDevice">;
|
|
40
40
|
export interface OpenRemoteRuntimeSessionEvents {
|
|
@@ -125,6 +125,10 @@ export class RemoteRuntimeDispatcher {
|
|
|
125
125
|
return await this.withSessions(request, (sessions) => sessions.agentOptions());
|
|
126
126
|
case "session.launchOptions.list":
|
|
127
127
|
return await this.withSessions(request, (sessions) => sessions.launchOptions());
|
|
128
|
+
case "session.execution.get":
|
|
129
|
+
return await this.withSessions(request, (sessions) => sessions.executionSettings(request.params.sessionId));
|
|
130
|
+
case "session.execution.update":
|
|
131
|
+
return await this.withSessions(request, (sessions) => sessions.updateExecutionSettings(request.params));
|
|
128
132
|
case "session.create":
|
|
129
133
|
return await this.withSessions(request, (sessions) => sessions.create(request.params));
|
|
130
134
|
case "session.fork":
|
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:
|
|
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: () =>
|
|
270
|
-
|
|
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;
|