@rynx-ai/server 0.1.0 → 0.1.9

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.
Files changed (63) hide show
  1. package/dist/browser-surface-control-queue.d.ts +42 -0
  2. package/dist/browser-surface-control-queue.js +187 -0
  3. package/dist/browser-surface-ws.d.ts +14 -0
  4. package/dist/browser-surface-ws.js +355 -0
  5. package/dist/control-api.d.ts +59 -4
  6. package/dist/control-api.js +2213 -132
  7. package/dist/control-web/assets/highlighted-body-OFNGDK62-e-w61YLy.js +1 -0
  8. package/dist/control-web/assets/index-BQ7XVBKO.css +32 -0
  9. package/dist/control-web/assets/index-ClpzuBJx.js +606 -0
  10. package/dist/control-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  11. package/dist/control-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  12. package/dist/control-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  13. package/dist/control-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  14. package/dist/control-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  15. package/dist/control-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  16. package/dist/control-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  17. package/dist/control-web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  18. package/dist/control-web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  19. package/dist/control-web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  20. package/dist/control-web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  21. package/dist/control-web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  22. package/dist/control-web/assets/mermaid-GHXKKRXX-DZn7Hbr2.js +131 -0
  23. package/dist/control-web/assets/rynx-wordmark-LL1QRYHD.png +0 -0
  24. package/dist/control-web/favicon.png +0 -0
  25. package/dist/control-web/favicon.svg +38 -0
  26. package/dist/control-web/index.html +16 -0
  27. package/dist/control-web-dist.d.ts +1 -1
  28. package/dist/control-web-dist.js +19 -14
  29. package/dist/desktop-browser-host.d.ts +153 -0
  30. package/dist/desktop-browser-host.js +936 -0
  31. package/dist/direct-runtime-browser-surface-server.d.ts +74 -0
  32. package/dist/direct-runtime-browser-surface-server.js +821 -0
  33. package/dist/direct-runtime-server.d.ts +137 -0
  34. package/dist/direct-runtime-server.js +1931 -0
  35. package/dist/emulator-surface-ws.d.ts +6 -0
  36. package/dist/emulator-surface-ws.js +197 -0
  37. package/dist/machine-session-service.d.ts +182 -0
  38. package/dist/machine-session-service.js +559 -0
  39. package/dist/provider-cli-host.d.ts +10 -0
  40. package/dist/provider-cli-host.js +1 -0
  41. package/dist/remote-runtime-dispatcher.d.ts +94 -0
  42. package/dist/remote-runtime-dispatcher.js +380 -0
  43. package/dist/remote-runtime-session-projection.d.ts +19 -0
  44. package/dist/remote-runtime-session-projection.js +556 -0
  45. package/dist/remote-runtime.d.ts +22 -0
  46. package/dist/remote-runtime.js +32 -0
  47. package/dist/runtime-web-auth.d.ts +21 -0
  48. package/dist/runtime-web-auth.js +92 -0
  49. package/dist/server.d.ts +292 -10
  50. package/dist/server.js +314 -98
  51. package/dist/session-browser-service.d.ts +218 -0
  52. package/dist/session-browser-service.js +686 -0
  53. package/dist/session-browser-surface-coordinator.d.ts +23 -0
  54. package/dist/session-browser-surface-coordinator.js +563 -0
  55. package/dist/session-emulator-service.d.ts +167 -0
  56. package/dist/session-emulator-service.js +464 -0
  57. package/dist/session-runtime-index.d.ts +17 -0
  58. package/dist/session-runtime-index.js +86 -0
  59. package/dist/session-terminal-host.d.ts +46 -0
  60. package/dist/session-terminal-host.js +252 -0
  61. package/dist/terminal-ws.d.ts +9 -20
  62. package/dist/terminal-ws.js +420 -97
  63. package/package.json +9 -7
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Process-local index of live Session state. Durable transcript items explain
3
+ * history; this index says which historical requests still have a real runtime
4
+ * waiter after a client reconnect. It intentionally starts empty after daemon
5
+ * restart because an old runner waiter cannot be resumed implicitly.
6
+ */
7
+ export class SessionRuntimeIndex {
8
+ sessions = new Map();
9
+ observe(sessionId, event) {
10
+ const state = this.state(sessionId);
11
+ switch (event.type) {
12
+ case "response.created":
13
+ state.activeResponseIds.add(event.responseId);
14
+ state.status = "running";
15
+ return;
16
+ case "response.completed":
17
+ state.activeResponseIds.delete(event.responseId);
18
+ this.removeResponseInteractions(state, event.responseId);
19
+ state.status = state.activeResponseIds.size > 0 ? "running" : "idle";
20
+ return;
21
+ case "response.failed":
22
+ state.activeResponseIds.delete(event.responseId);
23
+ this.removeResponseInteractions(state, event.responseId);
24
+ // Response failure is Turn-local. It must not poison a reusable Session.
25
+ state.status = state.activeResponseIds.size > 0 ? "running" : "idle";
26
+ return;
27
+ case "session.status":
28
+ state.status =
29
+ state.pendingInteractions.size > 0 || state.activeResponseIds.size > 0
30
+ ? "running"
31
+ : event.status;
32
+ return;
33
+ case "session.interaction.requested":
34
+ state.pendingInteractions.set(event.interaction.interactionId, {
35
+ responseId: event.responseId,
36
+ interaction: event.interaction,
37
+ });
38
+ state.activeResponseIds.add(event.responseId);
39
+ state.status = "running";
40
+ return;
41
+ case "session.interaction.resolved":
42
+ case "session.interaction.cancelled":
43
+ state.pendingInteractions.delete(event.interactionId);
44
+ state.status = state.activeResponseIds.size > 0 ? "running" : state.status;
45
+ return;
46
+ case "session.rotated":
47
+ state.activeResponseIds.clear();
48
+ state.pendingInteractions.clear();
49
+ state.status = "idle";
50
+ return;
51
+ default:
52
+ return;
53
+ }
54
+ }
55
+ snapshot(sessionId) {
56
+ const state = this.sessions.get(sessionId);
57
+ if (!state)
58
+ return { status: "idle", activeResponseIds: [], pendingInteractions: [] };
59
+ return {
60
+ status: state.pendingInteractions.size > 0 ? "running" : state.status,
61
+ activeResponseIds: [...state.activeResponseIds],
62
+ pendingInteractions: [...state.pendingInteractions.values()],
63
+ };
64
+ }
65
+ remove(sessionId) {
66
+ this.sessions.delete(sessionId);
67
+ }
68
+ state(sessionId) {
69
+ let state = this.sessions.get(sessionId);
70
+ if (!state) {
71
+ state = {
72
+ status: "idle",
73
+ activeResponseIds: new Set(),
74
+ pendingInteractions: new Map(),
75
+ };
76
+ this.sessions.set(sessionId, state);
77
+ }
78
+ return state;
79
+ }
80
+ removeResponseInteractions(state, responseId) {
81
+ for (const [interactionId, pending] of state.pendingInteractions) {
82
+ if (pending.responseId === responseId)
83
+ state.pendingInteractions.delete(interactionId);
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,46 @@
1
+ import { type RunnerManager } from "@rynx-ai/runtime";
2
+ export type SessionTerminalRole = "owner" | "read-only";
3
+ export interface SessionTerminalOpenOptions {
4
+ role: SessionTerminalRole;
5
+ cols: number;
6
+ rows: number;
7
+ signal?: AbortSignal;
8
+ }
9
+ export type SessionTerminalCloseReason = "client_closed" | "terminal_exited" | "not_live" | "unauthorized" | "capacity" | "backpressure" | "internal";
10
+ export interface SessionTerminalCloseInfo {
11
+ reason: SessionTerminalCloseReason;
12
+ exitCode?: number;
13
+ }
14
+ /** One attachment; close detaches this client and never kills the tmux pane. */
15
+ export interface SessionTerminalAttachment extends AsyncIterable<Uint8Array> {
16
+ readonly sessionId: string;
17
+ readonly role: SessionTerminalRole;
18
+ readonly closed: Promise<SessionTerminalCloseInfo>;
19
+ write(data: Uint8Array): void | Promise<void>;
20
+ resize(cols: number, rows: number): void | Promise<void>;
21
+ close(): Promise<void>;
22
+ }
23
+ /** Transport-neutral live terminal application port used by Local and Direct. */
24
+ export interface SessionTerminalHost {
25
+ open(sessionId: string, options: SessionTerminalOpenOptions): Promise<SessionTerminalAttachment>;
26
+ }
27
+ export type SessionTerminalOpenErrorCode = "invalid_request" | "not_live" | "unauthorized" | "capacity" | "method_not_found" | "internal";
28
+ export declare class SessionTerminalOpenError extends Error {
29
+ readonly code: SessionTerminalOpenErrorCode;
30
+ constructor(code: SessionTerminalOpenErrorCode, message: string, options?: ErrorOptions);
31
+ }
32
+ export declare class SessionTerminalStreamError extends Error {
33
+ readonly reason: Exclude<SessionTerminalCloseReason, "client_closed" | "terminal_exited">;
34
+ constructor(reason: Exclude<SessionTerminalCloseReason, "client_closed" | "terminal_exited">, message: string, options?: ErrorOptions);
35
+ }
36
+ export interface RunnerSessionTerminalHostOptions {
37
+ runnerManager: RunnerManager;
38
+ /** Resolve only a currently live Session. Undefined rejects before a runner can spawn. */
39
+ resolveLiveSession: (sessionId: string) => {
40
+ cwd: string;
41
+ } | undefined | Promise<{
42
+ cwd: string;
43
+ } | undefined>;
44
+ }
45
+ /** Adapt the runner-child terminal protocol without exposing cwd or commands to clients. */
46
+ export declare function createRunnerSessionTerminalHost(options: RunnerSessionTerminalHostOptions): SessionTerminalHost;
@@ -0,0 +1,252 @@
1
+ import { TerminalOpenError } from "@rynx-ai/runtime";
2
+ const MAX_QUEUED_OUTPUT_FRAMES = 128;
3
+ const MAX_QUEUED_OUTPUT_BYTES = 512 * 1024;
4
+ const MAX_TERMINAL_DIMENSION = 1_000;
5
+ export class SessionTerminalOpenError extends Error {
6
+ code;
7
+ constructor(code, message, options) {
8
+ super(message, options);
9
+ this.code = code;
10
+ this.name = "SessionTerminalOpenError";
11
+ }
12
+ }
13
+ export class SessionTerminalStreamError extends Error {
14
+ reason;
15
+ constructor(reason, message, options) {
16
+ super(message, options);
17
+ this.reason = reason;
18
+ this.name = "SessionTerminalStreamError";
19
+ }
20
+ }
21
+ /** Adapt the runner-child terminal protocol without exposing cwd or commands to clients. */
22
+ export function createRunnerSessionTerminalHost(options) {
23
+ return {
24
+ async open(sessionId, openOptions) {
25
+ validateOpen(sessionId, openOptions);
26
+ throwIfAborted(openOptions.signal);
27
+ const session = await options.resolveLiveSession(sessionId);
28
+ if (!session || typeof session.cwd !== "string" || session.cwd.trim().length === 0) {
29
+ throw new SessionTerminalOpenError("not_live", "Session terminal is not live");
30
+ }
31
+ throwIfAborted(openOptions.signal);
32
+ let parent;
33
+ try {
34
+ parent = options.runnerManager.openLiveTerminal(sessionId, {
35
+ role: openOptions.role,
36
+ cwd: session.cwd,
37
+ cols: openOptions.cols,
38
+ rows: openOptions.rows,
39
+ });
40
+ }
41
+ catch (error) {
42
+ if (error instanceof TerminalOpenError && error.code === "terminal_not_live") {
43
+ throw new SessionTerminalOpenError("not_live", "Session terminal is not live", {
44
+ cause: error,
45
+ });
46
+ }
47
+ throw new SessionTerminalOpenError("internal", "Session terminal could not be opened", { cause: error });
48
+ }
49
+ const attachment = new RunnerSessionTerminalAttachment(sessionId, parent, openOptions.signal);
50
+ try {
51
+ const opened = await waitFor(parent.ready, openOptions.signal);
52
+ attachment.opened(opened.role);
53
+ attachment.throwIfOpeningFailed();
54
+ return attachment;
55
+ }
56
+ catch (error) {
57
+ await attachment.close().catch(() => undefined);
58
+ if (isAbort(error)) {
59
+ throw new SessionTerminalOpenError("invalid_request", "Session terminal open was cancelled", { cause: error });
60
+ }
61
+ if (error instanceof TerminalOpenError && error.code === "terminal_not_live") {
62
+ throw new SessionTerminalOpenError("not_live", "Session terminal is not live", {
63
+ cause: error,
64
+ });
65
+ }
66
+ if (error instanceof SessionTerminalOpenError)
67
+ throw error;
68
+ throw new SessionTerminalOpenError("internal", "Session terminal could not be opened", { cause: error });
69
+ }
70
+ },
71
+ };
72
+ }
73
+ class RunnerSessionTerminalAttachment {
74
+ sessionId;
75
+ parent;
76
+ signal;
77
+ queue = [];
78
+ queuedBytes = 0;
79
+ waiter;
80
+ ended = false;
81
+ terminalError;
82
+ openingFailure;
83
+ currentRole;
84
+ resolveClosed;
85
+ closed;
86
+ onAbort;
87
+ constructor(sessionId, parent, signal) {
88
+ this.sessionId = sessionId;
89
+ this.parent = parent;
90
+ this.signal = signal;
91
+ this.currentRole = "read-only";
92
+ this.closed = new Promise((resolve) => {
93
+ this.resolveClosed = resolve;
94
+ });
95
+ this.onAbort = () => {
96
+ void this.close().catch(() => undefined);
97
+ };
98
+ parent.onData((chunk) => this.push(Uint8Array.from(Buffer.from(chunk, "utf8"))));
99
+ parent.onExit((info) => this.finish({ reason: "terminal_exited", exitCode: info.exitCode }));
100
+ if (signal?.aborted)
101
+ this.onAbort();
102
+ else
103
+ signal?.addEventListener("abort", this.onAbort, { once: true });
104
+ }
105
+ get role() {
106
+ return this.currentRole;
107
+ }
108
+ opened(role) {
109
+ this.currentRole = role;
110
+ }
111
+ throwIfOpeningFailed() {
112
+ if (this.openingFailure)
113
+ throw this.openingFailure;
114
+ if (this.ended) {
115
+ throw new SessionTerminalOpenError("internal", "Session terminal closed while opening");
116
+ }
117
+ }
118
+ [Symbol.asyncIterator]() {
119
+ return this;
120
+ }
121
+ next() {
122
+ const queued = this.queue.shift();
123
+ if (queued) {
124
+ this.queuedBytes -= queued.bytes;
125
+ return Promise.resolve({ done: false, value: queued.data });
126
+ }
127
+ if (this.terminalError !== undefined)
128
+ return Promise.reject(this.terminalError);
129
+ if (this.ended)
130
+ return Promise.resolve({ done: true, value: undefined });
131
+ if (this.waiter) {
132
+ return Promise.reject(new SessionTerminalStreamError("internal", "Concurrent reads from one Session terminal are not allowed"));
133
+ }
134
+ return new Promise((resolve, reject) => {
135
+ this.waiter = { resolve, reject };
136
+ });
137
+ }
138
+ async return() {
139
+ await this.close();
140
+ return { done: true, value: undefined };
141
+ }
142
+ write(data) {
143
+ if (this.ended || this.currentRole !== "owner" || data.byteLength === 0)
144
+ return;
145
+ this.parent.write(Buffer.from(data).toString("utf8"));
146
+ }
147
+ resize(cols, rows) {
148
+ validateDimensions(cols, rows);
149
+ if (!this.ended)
150
+ this.parent.resize(cols, rows);
151
+ }
152
+ async close() {
153
+ if (this.ended)
154
+ return;
155
+ this.parent.close();
156
+ this.finish({ reason: "client_closed" }, undefined, true);
157
+ }
158
+ push(data) {
159
+ if (this.ended || data.byteLength === 0)
160
+ return;
161
+ if (this.waiter) {
162
+ const waiter = this.waiter;
163
+ this.waiter = undefined;
164
+ waiter.resolve({ done: false, value: data });
165
+ return;
166
+ }
167
+ if (this.queue.length >= MAX_QUEUED_OUTPUT_FRAMES ||
168
+ this.queuedBytes + data.byteLength > MAX_QUEUED_OUTPUT_BYTES) {
169
+ const error = new SessionTerminalStreamError("backpressure", "Session terminal output exceeded its bounded queue");
170
+ this.openingFailure = new SessionTerminalOpenError("capacity", error.message, {
171
+ cause: error,
172
+ });
173
+ this.parent.close();
174
+ this.finish({ reason: "backpressure" }, error, true);
175
+ return;
176
+ }
177
+ const copy = Uint8Array.from(data);
178
+ this.queue.push({ data: copy, bytes: copy.byteLength });
179
+ this.queuedBytes += copy.byteLength;
180
+ }
181
+ finish(info, error, discardQueue = false) {
182
+ if (this.ended)
183
+ return;
184
+ this.ended = true;
185
+ this.terminalError = error;
186
+ this.signal?.removeEventListener("abort", this.onAbort);
187
+ if (discardQueue) {
188
+ this.queue.length = 0;
189
+ this.queuedBytes = 0;
190
+ }
191
+ const waiter = this.waiter;
192
+ this.waiter = undefined;
193
+ if (waiter) {
194
+ if (error === undefined)
195
+ waiter.resolve({ done: true, value: undefined });
196
+ else
197
+ waiter.reject(error);
198
+ }
199
+ this.resolveClosed(info);
200
+ }
201
+ }
202
+ function validateOpen(sessionId, options) {
203
+ if (!sessionId || sessionId.length > 256 || /[\u0000-\u001f\u007f]/.test(sessionId)) {
204
+ throw new SessionTerminalOpenError("invalid_request", "Session terminal id is invalid");
205
+ }
206
+ if (options.role !== "owner" && options.role !== "read-only") {
207
+ throw new SessionTerminalOpenError("invalid_request", "Session terminal role is invalid");
208
+ }
209
+ validateDimensions(options.cols, options.rows);
210
+ }
211
+ function validateDimensions(cols, rows) {
212
+ if (!Number.isSafeInteger(cols) ||
213
+ !Number.isSafeInteger(rows) ||
214
+ cols < 1 ||
215
+ rows < 1 ||
216
+ cols > MAX_TERMINAL_DIMENSION ||
217
+ rows > MAX_TERMINAL_DIMENSION) {
218
+ throw new SessionTerminalOpenError("invalid_request", `Session terminal dimensions must be integers within 1..${MAX_TERMINAL_DIMENSION}`);
219
+ }
220
+ }
221
+ async function waitFor(promise, signal) {
222
+ if (!signal)
223
+ return await promise;
224
+ throwIfAborted(signal);
225
+ return await new Promise((resolve, reject) => {
226
+ const onAbort = () => {
227
+ cleanup();
228
+ reject(signal.reason ?? abortError());
229
+ };
230
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
231
+ signal.addEventListener("abort", onAbort, { once: true });
232
+ void promise.then((value) => {
233
+ cleanup();
234
+ resolve(value);
235
+ }, (error) => {
236
+ cleanup();
237
+ reject(error);
238
+ });
239
+ });
240
+ }
241
+ function throwIfAborted(signal) {
242
+ if (signal?.aborted)
243
+ throw signal.reason ?? abortError();
244
+ }
245
+ function abortError() {
246
+ const error = new Error("Session terminal operation was aborted");
247
+ error.name = "AbortError";
248
+ return error;
249
+ }
250
+ function isAbort(error) {
251
+ return error instanceof Error && error.name === "AbortError";
252
+ }
@@ -1,25 +1,14 @@
1
- /**
2
- * WebSocket bridge for the live terminal (Phase C). Attaches to the existing
3
- * daemon `http.Server` via the `upgrade` event — no second port — and tunnels a
4
- * browser xterm session to a per-session terminal on the runner child through
5
- * {@link RunnerManager.openTerminal}.
6
- *
7
- * Wire protocol (mirrors omnigent's terminal attach):
8
- * - server → browser: BINARY frames = raw PTY output; TEXT frames = JSON
9
- * control (`{t:"opened",role}`, `{t:"exit"}`, `{t:"error",message}`).
10
- * - browser → server: BINARY frames = keystrokes; TEXT frames = JSON control
11
- * (`{t:"resize",cols,rows}` / `{t:"input",data}`).
12
- */
13
1
  import type { Server } from "node:http";
14
- import type { RunnerManager } from "@rynx-ai/runtime";
15
2
  import { WebSocketServer } from "ws";
3
+ import { type SessionTerminalHost } from "./session-terminal-host.js";
4
+ import type { RuntimeConnectionResolverHost } from "./server.js";
16
5
  export interface TerminalWsDeps {
17
- runnerManager: RunnerManager;
18
- /** Resolve a session's terminal working directory (sync). Defaults to cwd. */
19
- resolveCwd?: (sessionId: string) => string | undefined;
20
- /** Inner command for the pane; defaults child-side to the login shell. */
21
- command?: string;
22
- args?: string[];
6
+ /** Canonical Local/Direct Runtime resolver. Explicit selectors never fall back. */
7
+ runtimeConnectionResolver?: RuntimeConnectionResolverHost;
8
+ /** Compatibility host for the legacy local-only path. It receives identical auth. */
9
+ legacyLocalTerminalHost?: SessionTerminalHost;
10
+ /** WebSocket protocol ping interval; override only for tests. */
11
+ keepAliveMs?: number;
23
12
  }
24
- /** Attach the terminal WS upgrade handler to a running http.Server. */
13
+ /** Attach the authenticated Runtime-scoped terminal bridge to an HTTP server. */
25
14
  export declare function attachTerminalWs(server: Server, deps: TerminalWsDeps): WebSocketServer;