@braedonsaunders/appkit-desk 0.4.0

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/guest-agent.js ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The in-guest agent: PID 1's helper inside the microVM, speaking the framed
3
+ * JSON protocol over a vsock stream. This is the only new attack surface in
4
+ * the desk design, so it is deliberately small: a bounded frame decoder, a
5
+ * strict parser, a closed dispatch switch, and nothing else. Everything that
6
+ * touches the guest OS — spawning processes, driving the compositor, reading
7
+ * the clipboard — is injected as a handler so the message-handling core stays
8
+ * pure and unit-testable on any platform.
9
+ *
10
+ * The module depends only on `node:` built-ins and this package's own
11
+ * protocol module; it ships in the same package and is started by the guest's
12
+ * init.
13
+ */
14
+ import { Buffer } from 'node:buffer';
15
+ import { DEFAULT_MAX_FRAME_BYTES, DeskProtocolError, encodeFrame, FrameDecoder, parseGuestRequest, } from './protocol.js';
16
+ export function createGuestAgentCore(handlers, options = {}) {
17
+ const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
18
+ const decoder = new FrameDecoder({ maxFrameBytes });
19
+ async function respond(value) {
20
+ let request;
21
+ try {
22
+ request = parseGuestRequest(value);
23
+ }
24
+ catch (error) {
25
+ // The frame itself was well-formed JSON, so answering is safe; echo the
26
+ // id when one is recoverable so the host can settle its pending call.
27
+ const id = recoverId(value);
28
+ return { id, ok: false, error: errorMessage(error) };
29
+ }
30
+ try {
31
+ return { id: request.id, ok: true, result: await dispatch(request) };
32
+ }
33
+ catch (error) {
34
+ return { id: request.id, ok: false, error: errorMessage(error) };
35
+ }
36
+ }
37
+ async function dispatch(request) {
38
+ switch (request.op) {
39
+ case 'ping':
40
+ return { pong: true };
41
+ case 'capabilities':
42
+ return handlers.capabilities();
43
+ case 'exec':
44
+ return handlers.exec({
45
+ command: request.command,
46
+ args: request.args ?? [],
47
+ cwd: request.cwd ?? null,
48
+ env: request.env ?? null,
49
+ timeoutMs: request.timeoutMs ?? null,
50
+ });
51
+ case 'job-start':
52
+ return handlers.jobStart({
53
+ command: request.command,
54
+ args: request.args ?? [],
55
+ cwd: request.cwd ?? null,
56
+ env: request.env ?? null,
57
+ });
58
+ case 'job-signal':
59
+ await handlers.jobSignal({ jobId: request.jobId, signal: request.signal });
60
+ return {};
61
+ case 'screen-start':
62
+ await handlers.screenStart({ width: request.width, height: request.height });
63
+ return {};
64
+ case 'screen-stop':
65
+ await handlers.screenStop();
66
+ return {};
67
+ case 'observe':
68
+ return handlers.observe();
69
+ case 'input':
70
+ await handlers.input(request.input);
71
+ return {};
72
+ case 'a11y-invoke':
73
+ await handlers.a11yInvoke({ nodeId: request.nodeId, action: request.action });
74
+ return {};
75
+ case 'launch':
76
+ await handlers.launch({ appId: request.appId, args: request.args ?? [] });
77
+ return {};
78
+ case 'clipboard-read':
79
+ return handlers.clipboardRead();
80
+ case 'clipboard-write':
81
+ await handlers.clipboardWrite({ text: request.text });
82
+ return {};
83
+ case 'frames-start':
84
+ await handlers.framesStart({
85
+ fps: request.fps,
86
+ width: request.width,
87
+ height: request.height,
88
+ format: request.format,
89
+ });
90
+ return {};
91
+ case 'frames-stop':
92
+ await handlers.framesStop();
93
+ return {};
94
+ case 'video-start':
95
+ await handlers.videoStart({
96
+ fps: request.fps,
97
+ width: request.width,
98
+ height: request.height,
99
+ });
100
+ return {};
101
+ case 'video-stop':
102
+ await handlers.videoStop();
103
+ return {};
104
+ case 'handover-begin':
105
+ return handlers.handoverBegin({ ttlMs: request.ttlMs, scope: request.scope });
106
+ case 'handover-end':
107
+ await handlers.handoverEnd();
108
+ return {};
109
+ }
110
+ }
111
+ return {
112
+ async handleChunk(chunk) {
113
+ const values = decoder.push(chunk);
114
+ const responses = [];
115
+ for (const value of values) {
116
+ responses.push(encodeFrame(await respond(value), maxFrameBytes));
117
+ }
118
+ return responses;
119
+ },
120
+ encodeEvent(event) {
121
+ return encodeFrame(event, maxFrameBytes);
122
+ },
123
+ };
124
+ }
125
+ /**
126
+ * Bind the pure core to a real byte stream. This is the entire impure surface
127
+ * of the guest agent: read chunks, write responses, destroy the connection on
128
+ * a framing violation.
129
+ */
130
+ export function runGuestAgent(options) {
131
+ const { stream, handlers } = options;
132
+ const core = createGuestAgentCore(handlers, { maxFrameBytes: options.maxFrameBytes });
133
+ let queue = Promise.resolve();
134
+ stream.on('data', (chunk) => {
135
+ queue = queue
136
+ .then(async () => {
137
+ for (const frame of await core.handleChunk(chunk))
138
+ stream.write(frame);
139
+ })
140
+ .catch((error) => {
141
+ stream.destroy(error instanceof DeskProtocolError ? error : new DeskProtocolError(errorMessage(error)));
142
+ });
143
+ });
144
+ const closed = new Promise((resolvePromise) => {
145
+ stream.once('close', () => resolvePromise());
146
+ });
147
+ return {
148
+ sendEvent(event) {
149
+ stream.write(core.encodeEvent(event));
150
+ },
151
+ closed,
152
+ };
153
+ }
154
+ function recoverId(value) {
155
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
156
+ const id = value.id;
157
+ if (typeof id === 'string' && id.length > 0 && id.length <= 128)
158
+ return id;
159
+ }
160
+ return 'unknown';
161
+ }
162
+ function errorMessage(error) {
163
+ return error instanceof Error ? error.message : String(error);
164
+ }
165
+ //# sourceMappingURL=guest-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guest-agent.js","sourceRoot":"","sources":["../src/guest-agent.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The in-guest agent: PID 1's helper inside the microVM, speaking the framed\n * JSON protocol over a vsock stream. This is the only new attack surface in\n * the desk design, so it is deliberately small: a bounded frame decoder, a\n * strict parser, a closed dispatch switch, and nothing else. Everything that\n * touches the guest OS — spawning processes, driving the compositor, reading\n * the clipboard — is injected as a handler so the message-handling core stays\n * pure and unit-testable on any platform.\n *\n * The module depends only on `node:` built-ins and this package's own\n * protocol module; it ships in the same package and is started by the guest's\n * init.\n */\nimport { Buffer } from 'node:buffer'\nimport type { Duplex } from 'node:stream'\nimport {\n DEFAULT_MAX_FRAME_BYTES,\n DeskProtocolError,\n encodeFrame,\n FrameDecoder,\n parseGuestRequest,\n type GuestEventMessage,\n type GuestExecResult,\n type GuestObservation,\n type GuestInput,\n type GuestRequest,\n type GuestResponse,\n} from './protocol'\nimport type { DeskFrameFormat, DeskHandoverScope } from './events'\n\nexport interface GuestExecCall {\n command: string\n args: readonly string[]\n cwd: string | null\n env: Readonly<Record<string, string>> | null\n timeoutMs: number | null\n}\n\nexport interface GuestJobStartCall {\n command: string\n args: readonly string[]\n cwd: string | null\n env: Readonly<Record<string, string>> | null\n}\n\n/**\n * The operations the guest environment implements. Handlers own all contact\n * with the OS; the core owns all contact with the wire.\n */\nexport interface GuestAgentHandlers {\n exec(call: GuestExecCall): Promise<GuestExecResult>\n jobStart(call: GuestJobStartCall): Promise<{ jobId: string }>\n jobSignal(call: { jobId: string; signal: 'SIGTERM' | 'SIGKILL' }): Promise<void>\n screenStart(call: { width: number; height: number }): Promise<void>\n screenStop(): Promise<void>\n observe(): Promise<GuestObservation>\n input(action: GuestInput): Promise<void>\n a11yInvoke(call: { nodeId: string; action: string }): Promise<void>\n launch(call: { appId: string; args: readonly string[] }): Promise<void>\n clipboardRead(): Promise<{ text: string }>\n clipboardWrite(call: { text: string }): Promise<void>\n /**\n * `format` absent means the host did not ask, so the guest picks — an older\n * host never sends it and must still get a stream it can decode.\n */\n framesStart(call: {\n fps: number\n width: number\n height: number\n format?: DeskFrameFormat\n }): Promise<void>\n framesStop(): Promise<void>\n /**\n * Start encoding the screen as video. Separate from `framesStart` because it\n * is a different job with a different transport shape: one long-lived encoder\n * whose output only means anything in order, rather than independent pictures.\n */\n videoStart(call: { fps: number; width: number; height: number }): Promise<void>\n videoStop(): Promise<void>\n handoverBegin(call: { ttlMs: number; scope: DeskHandoverScope }): Promise<{ url: string }>\n handoverEnd(): Promise<void>\n capabilities(): Promise<{ virtioGpu: boolean }>\n}\n\nexport interface GuestAgentCore {\n /**\n * Consume one chunk of the byte stream and return the frames to write back,\n * in order. Malformed requests produce error responses; a framing violation\n * (oversized or non-JSON frame) throws, and the caller must drop the\n * connection — a length-prefixed stream cannot be resynchronized once its\n * lengths are untrusted.\n */\n handleChunk(chunk: Buffer): Promise<Buffer[]>\n /** Encode an unsolicited guest-to-host event frame. */\n encodeEvent(event: GuestEventMessage): Buffer\n}\n\nexport function createGuestAgentCore(\n handlers: GuestAgentHandlers,\n options: { maxFrameBytes?: number } = {},\n): GuestAgentCore {\n const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES\n const decoder = new FrameDecoder({ maxFrameBytes })\n\n async function respond(value: unknown): Promise<GuestResponse> {\n let request: GuestRequest\n try {\n request = parseGuestRequest(value)\n } catch (error) {\n // The frame itself was well-formed JSON, so answering is safe; echo the\n // id when one is recoverable so the host can settle its pending call.\n const id = recoverId(value)\n return { id, ok: false, error: errorMessage(error) }\n }\n try {\n return { id: request.id, ok: true, result: await dispatch(request) }\n } catch (error) {\n return { id: request.id, ok: false, error: errorMessage(error) }\n }\n }\n\n async function dispatch(request: GuestRequest): Promise<unknown> {\n switch (request.op) {\n case 'ping':\n return { pong: true }\n case 'capabilities':\n return handlers.capabilities()\n case 'exec':\n return handlers.exec({\n command: request.command,\n args: request.args ?? [],\n cwd: request.cwd ?? null,\n env: request.env ?? null,\n timeoutMs: request.timeoutMs ?? null,\n })\n case 'job-start':\n return handlers.jobStart({\n command: request.command,\n args: request.args ?? [],\n cwd: request.cwd ?? null,\n env: request.env ?? null,\n })\n case 'job-signal':\n await handlers.jobSignal({ jobId: request.jobId, signal: request.signal })\n return {}\n case 'screen-start':\n await handlers.screenStart({ width: request.width, height: request.height })\n return {}\n case 'screen-stop':\n await handlers.screenStop()\n return {}\n case 'observe':\n return handlers.observe()\n case 'input':\n await handlers.input(request.input)\n return {}\n case 'a11y-invoke':\n await handlers.a11yInvoke({ nodeId: request.nodeId, action: request.action })\n return {}\n case 'launch':\n await handlers.launch({ appId: request.appId, args: request.args ?? [] })\n return {}\n case 'clipboard-read':\n return handlers.clipboardRead()\n case 'clipboard-write':\n await handlers.clipboardWrite({ text: request.text })\n return {}\n case 'frames-start':\n await handlers.framesStart({\n fps: request.fps,\n width: request.width,\n height: request.height,\n format: request.format,\n })\n return {}\n case 'frames-stop':\n await handlers.framesStop()\n return {}\n case 'video-start':\n await handlers.videoStart({\n fps: request.fps,\n width: request.width,\n height: request.height,\n })\n return {}\n case 'video-stop':\n await handlers.videoStop()\n return {}\n case 'handover-begin':\n return handlers.handoverBegin({ ttlMs: request.ttlMs, scope: request.scope })\n case 'handover-end':\n await handlers.handoverEnd()\n return {}\n }\n }\n\n return {\n async handleChunk(chunk: Buffer): Promise<Buffer[]> {\n const values = decoder.push(chunk)\n const responses: Buffer[] = []\n for (const value of values) {\n responses.push(encodeFrame(await respond(value), maxFrameBytes))\n }\n return responses\n },\n encodeEvent(event: GuestEventMessage): Buffer {\n return encodeFrame(event, maxFrameBytes)\n },\n }\n}\n\nexport interface RunningGuestAgent {\n /** Push an unsolicited event (job exit, frame, focus change) to the host. */\n sendEvent(event: GuestEventMessage): void\n /** Resolves when the connection has closed. */\n closed: Promise<void>\n}\n\n/**\n * Bind the pure core to a real byte stream. This is the entire impure surface\n * of the guest agent: read chunks, write responses, destroy the connection on\n * a framing violation.\n */\nexport function runGuestAgent(options: {\n stream: Duplex\n handlers: GuestAgentHandlers\n maxFrameBytes?: number\n}): RunningGuestAgent {\n const { stream, handlers } = options\n const core = createGuestAgentCore(handlers, { maxFrameBytes: options.maxFrameBytes })\n let queue: Promise<void> = Promise.resolve()\n\n stream.on('data', (chunk: Buffer) => {\n queue = queue\n .then(async () => {\n for (const frame of await core.handleChunk(chunk)) stream.write(frame)\n })\n .catch((error: unknown) => {\n stream.destroy(\n error instanceof DeskProtocolError ? error : new DeskProtocolError(errorMessage(error)),\n )\n })\n })\n\n const closed = new Promise<void>((resolvePromise) => {\n stream.once('close', () => resolvePromise())\n })\n\n return {\n sendEvent(event: GuestEventMessage): void {\n stream.write(core.encodeEvent(event))\n },\n closed,\n }\n}\n\nfunction recoverId(value: unknown): string {\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const id = (value as Record<string, unknown>).id\n if (typeof id === 'string' && id.length > 0 && id.length <= 128) return id\n }\n return 'unknown'\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n"]}
package/host.d.ts ADDED
@@ -0,0 +1,325 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { type DeskLauncherIdentity } from './plan.js';
3
+ import { type DeskBackend } from './backend.js';
4
+ import type { A11yNode, DeskFrameFormat, DeskHandoverScope, DeskPoint, DeskPointerButton, DeskPorts, DeskVideoChunkKind, WindowInfo } from './events.js';
5
+ export declare const DEFAULT_CAPACITY = 4;
6
+ export declare const DEFAULT_IDLE_SUSPEND_MS: number;
7
+ export declare const DEFAULT_LEASE_MS: number;
8
+ export declare const DEFAULT_CID_BASE = 100;
9
+ export interface DeskHostOptions {
10
+ /** Root of the golden base image and per-desk assets. */
11
+ imageRoot: string;
12
+ vmmPath?: string;
13
+ kvmPath?: string;
14
+ /** Guest kernel; defaults to `<imageRoot>/vmlinux`. */
15
+ kernelPath?: string;
16
+ /**
17
+ * Initramfs the guest kernel boots with. A modular distro cloud kernel needs
18
+ * one to mount root; omit only for a kernel with virtio/ext4 built in. When
19
+ * unset it defaults to `<imageRoot>/initrd` if that file exists, else none.
20
+ */
21
+ initramfsPath?: string;
22
+ /**
23
+ * Kernel command line every desk boots with. Defaults to
24
+ * `DEFAULT_KERNEL_CMDLINE`; override it for partitioned cloud images whose
25
+ * root is not `/dev/vda` (e.g. `root=/dev/vda3 rw quiet`).
26
+ */
27
+ kernelCmdline?: string;
28
+ /** Directory holding vsock and API control sockets. */
29
+ runtimeDir?: string;
30
+ launcherIdentity?: DeskLauncherIdentity;
31
+ backend?: DeskBackend;
32
+ ports: DeskPorts;
33
+ /** Hard cap on concurrently resident desks; starts beyond it queue FIFO. */
34
+ capacity?: number;
35
+ idleSuspendMs?: number;
36
+ defaultLeaseMs?: number;
37
+ /** First guest vsock CID handed out; each boot takes the next one. */
38
+ cidBase?: number;
39
+ now?: () => number;
40
+ pathExists?: (path: string) => boolean;
41
+ deviceExists?: (path: string) => boolean;
42
+ }
43
+ export interface DeskStartOptions {
44
+ deskId: string;
45
+ /** Absolute path of the golden base image this desk boots from. */
46
+ baseImage: string;
47
+ /** Absolute path of this desk's copy-on-write overlay. */
48
+ overlayPath: string;
49
+ memoryMb?: number;
50
+ vcpus?: number;
51
+ leaseMs?: number;
52
+ network?: {
53
+ tapDevice?: string;
54
+ macAddress?: string;
55
+ };
56
+ }
57
+ export interface DeskExecOptions {
58
+ command: string;
59
+ args?: readonly string[];
60
+ cwd?: string;
61
+ env?: Readonly<Record<string, string>>;
62
+ timeoutMs?: number;
63
+ /** Survive the call as a background job; the job dies with the lease. */
64
+ keepAlive?: boolean;
65
+ }
66
+ export interface DeskExecSnapshot {
67
+ deskId: string;
68
+ command: string;
69
+ args: readonly string[];
70
+ exitCode: number | null;
71
+ signal: string | null;
72
+ stdout: string;
73
+ stderr: string;
74
+ truncated: boolean;
75
+ startedAt: string;
76
+ finishedAt: string;
77
+ }
78
+ export interface DeskJob {
79
+ jobId: string;
80
+ deskId: string;
81
+ command: string;
82
+ args: readonly string[];
83
+ startedAt: string;
84
+ status: 'running' | 'exited';
85
+ exitCode: number | null;
86
+ signal: string | null;
87
+ }
88
+ export interface DeskObservation {
89
+ png: Buffer;
90
+ width: number;
91
+ height: number;
92
+ a11y: A11yNode | null;
93
+ windows: readonly WindowInfo[];
94
+ focused: WindowInfo | null;
95
+ }
96
+ export interface DeskFrame {
97
+ seq: number;
98
+ width: number;
99
+ height: number;
100
+ data: Buffer;
101
+ /**
102
+ * How `data` is encoded. A consumer must not assume PNG — it builds the
103
+ * media type it hands on (a `data:` URL, a decoder) from this.
104
+ */
105
+ format: DeskFrameFormat;
106
+ at: string;
107
+ }
108
+ /**
109
+ * One unit of the live video stream: the init segment, or one media fragment.
110
+ *
111
+ * Video is deliberately NOT modelled as a kind of frame. A frame is a picture
112
+ * that stands alone; a video chunk is meaningless without the init segment
113
+ * before it and, for a media chunk, without the keyframe its group of pictures
114
+ * began at. Pretending otherwise produces a consumer that appends bytes in any
115
+ * order and renders black with no error to explain it.
116
+ */
117
+ export interface DeskVideoChunk {
118
+ seq: number;
119
+ kind: DeskVideoChunkKind;
120
+ /** RFC 6381, e.g. `avc1.42C020`. What a MediaSource must be told. */
121
+ codec: string;
122
+ width: number;
123
+ height: number;
124
+ /** True when a `media` chunk begins at a sync sample. Always false on `init`. */
125
+ keyframe: boolean;
126
+ data: Buffer;
127
+ at: string;
128
+ }
129
+ export interface DeskScreenInput {
130
+ move(x: number, y: number): Promise<void>;
131
+ click(x: number, y: number, button?: DeskPointerButton): Promise<void>;
132
+ type(text: string): Promise<void>;
133
+ key(combo: string): Promise<void>;
134
+ scroll(x: number, y: number, dx: number, dy: number): Promise<void>;
135
+ drag(from: DeskPoint, to: DeskPoint): Promise<void>;
136
+ }
137
+ export interface DeskScreenHandle {
138
+ observe(): Promise<DeskObservation>;
139
+ /**
140
+ * Coordinate contract: all coordinates are in the pixel space of the most
141
+ * recent view of the screen — an `observe()`, a frame from `frames()`, or a
142
+ * chunk from `video()` — one to one. All three anchor it, because the caller
143
+ * aims at whatever it was last shown, and a live viewer is shown a stream
144
+ * rather than an observation. That holds whatever the encoding is: a lossy
145
+ * frame and an H.264 fragment are both true-size views of the screen, so they
146
+ * anchor exactly as a lossless PNG does. Input before any view, or outside
147
+ * its bounds, throws.
148
+ */
149
+ input: DeskScreenInput;
150
+ a11y: {
151
+ invoke(nodeId: string, action: string): Promise<void>;
152
+ };
153
+ launch(appId: string, args?: readonly string[]): Promise<void>;
154
+ clipboard: {
155
+ read(): Promise<string>;
156
+ write(text: string): Promise<void>;
157
+ };
158
+ /**
159
+ * The live frame stream. `format` picks how the guest encodes each frame and
160
+ * is a bytes-for-fidelity trade: `jpeg` is roughly an order of magnitude
161
+ * smaller per frame, which is what a human driving the screen needs, while
162
+ * `png` stays exact. Omitted leaves the choice to the guest. `observe()` is
163
+ * unaffected and always lossless.
164
+ *
165
+ * The stream also ends when the desk reconnects to its guest agent: the
166
+ * capture behind it did not survive, so the iterator completes rather than
167
+ * waiting on frames that are never coming. Start another one to resume.
168
+ */
169
+ frames(options?: {
170
+ fps?: number;
171
+ width?: number;
172
+ height?: number;
173
+ format?: DeskFrameFormat;
174
+ }): AsyncIterable<DeskFrame>;
175
+ /**
176
+ * The live screen as encoded VIDEO rather than as a sequence of pictures.
177
+ *
178
+ * This is what a person driving the desk should watch. `frames()` ships a
179
+ * whole independent image per tick; a video codec ships the DIFFERENCE, and a
180
+ * desktop is mostly still — which is roughly two orders of magnitude fewer
181
+ * bytes on the transport between the guest and here, the one place the live
182
+ * view was actually bounded.
183
+ *
184
+ * The stream anchors the coordinate space exactly as `observe()` and
185
+ * `frames()` do, from the first chunk onwards, because a viewer driving by
186
+ * video is aiming at what the video showed it.
187
+ *
188
+ * Masked like frames: nothing is emitted while a handover is active. And
189
+ * ended like frames when the desk reconnects to its guest agent — a video
190
+ * consumer resumed on a fresh encoder, without its init segment and away
191
+ * from a keyframe, decodes nothing at all and says so nowhere.
192
+ */
193
+ video(options?: {
194
+ fps?: number;
195
+ width?: number;
196
+ height?: number;
197
+ }): AsyncIterable<DeskVideoChunk>;
198
+ handover: {
199
+ begin(options: {
200
+ ttlMs: number;
201
+ scope?: DeskHandoverScope;
202
+ actor?: string;
203
+ }): Promise<{
204
+ url: string;
205
+ }>;
206
+ end(): Promise<void>;
207
+ readonly active: boolean;
208
+ };
209
+ }
210
+ export interface DeskScreenService {
211
+ start(options: {
212
+ width: number;
213
+ height: number;
214
+ }): Promise<DeskScreenHandle>;
215
+ stop(): Promise<void>;
216
+ readonly running: boolean;
217
+ }
218
+ /**
219
+ * One leased desk.
220
+ *
221
+ * The handle stays valid across a lost guest connection: the host reconnects
222
+ * underneath it and later calls work, so a guest agent restart or a dropped
223
+ * vsock bridge no longer costs the rest of the lease. Two consequences a
224
+ * caller must actually handle:
225
+ *
226
+ * - A call that was IN FLIGHT when the connection dropped rejects with
227
+ * `DeskRequestFateUnknownError` — it may already have run in the guest.
228
+ * Nothing retries it for you.
229
+ * - A reconnect does not restore guest-side capture state. Live `frames()` and
230
+ * `video()` iterators END, and the coordinate anchor is dropped, so the
231
+ * caller must `observe()` (or take a new frame) before aiming again.
232
+ *
233
+ * When the connection cannot be recovered at all the desk is suspended, so the
234
+ * handle goes dead and `resume()` boots a fresh one — rather than the handle
235
+ * answering "no longer connected" for the rest of the lease.
236
+ */
237
+ export interface DeskHandle {
238
+ readonly deskId: string;
239
+ renewLease(ms: number): void;
240
+ exec(options: DeskExecOptions & {
241
+ keepAlive: true;
242
+ }): Promise<DeskJob>;
243
+ exec(options: DeskExecOptions): Promise<DeskExecSnapshot>;
244
+ /** The keepAlive processes still running on this desk. */
245
+ jobs(): DeskJob[];
246
+ screen: DeskScreenService;
247
+ }
248
+ export interface DeskHostStats {
249
+ /** Desks currently resident (booted or booting) against the capacity cap. */
250
+ resident: number;
251
+ /** Starts waiting in the FIFO queue for capacity. */
252
+ queued: number;
253
+ capacity: number;
254
+ suspended: number;
255
+ lastStartedAt: string | null;
256
+ lastSuspendedAt: string | null;
257
+ /**
258
+ * Times a resident desk lost its guest connection mid-lease and got it back.
259
+ * One is a blip; a number that climbs is a desk FLAPPING — a guest agent
260
+ * restarting in a loop, a failing vsock bridge — and that is the difference
261
+ * between "the desk is slow" and a diagnosis.
262
+ */
263
+ reconnects: number;
264
+ lastReconnectAt: string | null;
265
+ /** Which desk reconnected last: enough to go and look at the right one. */
266
+ lastReconnectDeskId: string | null;
267
+ lastError: string | null;
268
+ }
269
+ export interface DeskHost {
270
+ start(options: DeskStartOptions): Promise<DeskHandle>;
271
+ resume(deskId: string): Promise<DeskHandle>;
272
+ suspend(deskId: string): Promise<void>;
273
+ destroy(deskId: string): Promise<void>;
274
+ stats(): DeskHostStats;
275
+ /**
276
+ * Enforce every time-based rule now: expire handovers past their TTL and
277
+ * suspend desks past their lease or idle deadline. Returns the number of
278
+ * desks suspended. Timers call this automatically; tests with an injected
279
+ * clock call it directly.
280
+ */
281
+ sweep(): Promise<number>;
282
+ }
283
+ export declare function createDeskHost(options: DeskHostOptions): DeskHost;
284
+ export interface VerifyDeskHostOptions {
285
+ kernelPath: string;
286
+ /** Initramfs for the probe VM; needed for a modular kernel, as at boot. */
287
+ initramfsPath?: string;
288
+ /**
289
+ * Kernel command line for the probe VM. It must match what real desks boot
290
+ * with: a probe that panics because its root is on another partition
291
+ * reports a host that cannot run desks, which is indistinguishable from a
292
+ * host that genuinely cannot.
293
+ */
294
+ kernelCmdline?: string;
295
+ baseImagePath: string;
296
+ vmmPath?: string;
297
+ kvmPath?: string;
298
+ /** Where the throwaway probe overlay is created; defaults to the OS tmpdir. */
299
+ scratchDir?: string;
300
+ runtimeDir?: string;
301
+ backend?: DeskBackend;
302
+ launcherIdentity?: DeskLauncherIdentity;
303
+ platform?: NodeJS.Platform;
304
+ pathExists?: (path: string) => boolean;
305
+ deviceExists?: (path: string) => boolean;
306
+ idFactory?: () => string;
307
+ }
308
+ export interface DeskHostVerification {
309
+ /** Whether this host can serve desks: KVM present and the guest reachable. */
310
+ supported: boolean;
311
+ vmmPath: string;
312
+ kvm: boolean;
313
+ vsock: boolean;
314
+ virtioGpu: boolean;
315
+ }
316
+ /**
317
+ * Boot-time probe. Distinguishes a host that is unusable — wrong platform,
318
+ * missing VMM, missing images — which throws, from capabilities that are
319
+ * merely absent, which are reported as booleans: `kvm` (no boot possible
320
+ * without it), `vsock` (the guest agent answered), and `virtioGpu` (a screen
321
+ * would have hardware-backed rendering). It boots a throwaway microVM through
322
+ * the backend, asks it two questions, and discards it.
323
+ */
324
+ export declare function verifyDeskHost(options: VerifyDeskHostOptions): Promise<DeskHostVerification>;
325
+ //# sourceMappingURL=host.d.ts.map
package/host.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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"}