@elizaos/plugin-remote-desktop 2.0.3-beta.6 → 2.0.3-beta.7

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/index.js ADDED
@@ -0,0 +1,1056 @@
1
+ // src/actions/remote-desktop.ts
2
+ import {
3
+ requireConfirmation,
4
+ resolveActionArgs
5
+ } from "@elizaos/core";
6
+
7
+ // src/lifeops/remote-desktop.ts
8
+ import { execFile, spawn } from "node:child_process";
9
+ import { randomInt, randomUUID } from "node:crypto";
10
+ import { promisify } from "node:util";
11
+ import { logger } from "@elizaos/core";
12
+ var execFileAsync = promisify(execFile);
13
+
14
+ class RemoteDesktopError extends Error {
15
+ backend;
16
+ constructor(message, backend) {
17
+ super(message);
18
+ this.name = "RemoteDesktopError";
19
+ this.backend = backend;
20
+ }
21
+ }
22
+ var sessions = new Map;
23
+ var DEFAULT_VNC_PORT = 5900;
24
+ var DEFAULT_SESSION_MINUTES = 60;
25
+ function isMockRemoteDesktopEnabled() {
26
+ const explicit = process.env.ELIZA_TEST_REMOTE_DESKTOP_BACKEND?.trim();
27
+ if (explicit) {
28
+ const normalized = explicit.toLowerCase();
29
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on" || normalized === "fixture";
30
+ }
31
+ return false;
32
+ }
33
+ function resolveConfig(config, env = process.env) {
34
+ return {
35
+ preferredBackend: config?.preferredBackend,
36
+ tailscaleNodeName: config?.tailscaleNodeName ?? (env.ELIZA_TAILSCALE_NODE?.trim() || undefined),
37
+ ngrokAuthToken: config?.ngrokAuthToken ?? (env.ELIZA_NGROK_AUTH_TOKEN?.trim() || undefined),
38
+ vncPort: config?.vncPort ?? DEFAULT_VNC_PORT,
39
+ sessionDurationMinutes: config?.sessionDurationMinutes ?? DEFAULT_SESSION_MINUTES
40
+ };
41
+ }
42
+ async function probeTailscale() {
43
+ try {
44
+ const { stdout } = await execFileAsync("tailscale", ["status", "--json"], {
45
+ timeout: 3000
46
+ });
47
+ const parsed = JSON.parse(stdout);
48
+ const authenticated = parsed.BackendState === "Running";
49
+ const hostname = parsed.Self?.DNSName?.replace(/\.$/, "") || parsed.Self?.HostName;
50
+ return { authenticated, hostname };
51
+ } catch {
52
+ return { authenticated: false };
53
+ }
54
+ }
55
+ async function probeLocalVncServer() {
56
+ if (process.platform === "darwin") {
57
+ try {
58
+ const { stdout } = await execFileAsync("launchctl", ["list", "com.apple.screensharing"], { timeout: 2000 });
59
+ return stdout.includes("com.apple.screensharing");
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ if (process.platform === "linux") {
65
+ try {
66
+ await execFileAsync("which", ["x11vnc"], { timeout: 2000 });
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+ async function probeNgrok(token) {
75
+ if (!token)
76
+ return false;
77
+ try {
78
+ await execFileAsync("ngrok", ["version"], { timeout: 2000 });
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+ async function detectRemoteDesktopBackend(config) {
85
+ if (isMockRemoteDesktopEnabled()) {
86
+ return "tailscale-vnc";
87
+ }
88
+ const resolved = resolveConfig(config);
89
+ if (resolved.preferredBackend === "none")
90
+ return "none";
91
+ if (resolved.preferredBackend) {
92
+ const available = await backendAvailable(resolved.preferredBackend, resolved.ngrokAuthToken);
93
+ return available ? resolved.preferredBackend : "none";
94
+ }
95
+ const tailscale = await probeTailscale();
96
+ if (tailscale.authenticated) {
97
+ if (await probeLocalVncServer())
98
+ return "tailscale-vnc";
99
+ return "tailscale-ssh";
100
+ }
101
+ if (await probeNgrok(resolved.ngrokAuthToken))
102
+ return "ngrok-vnc";
103
+ return "none";
104
+ }
105
+ async function backendAvailable(backend, ngrokToken) {
106
+ if (backend === "none")
107
+ return true;
108
+ if (backend === "tailscale-vnc") {
109
+ const ts = await probeTailscale();
110
+ return ts.authenticated && await probeLocalVncServer();
111
+ }
112
+ if (backend === "tailscale-ssh") {
113
+ const ts = await probeTailscale();
114
+ return ts.authenticated;
115
+ }
116
+ if (backend === "ngrok-vnc") {
117
+ return probeNgrok(ngrokToken);
118
+ }
119
+ return false;
120
+ }
121
+ function generatePairingCode() {
122
+ return randomInt(0, 1e6).toString().padStart(6, "0");
123
+ }
124
+ function scheduleExpiry(id, durationMs) {
125
+ return setTimeout(() => {
126
+ endRemoteSession(id).catch((error) => {
127
+ logger.warn({
128
+ boundary: "lifeops",
129
+ integration: "remote-desktop",
130
+ sessionId: id,
131
+ err: error instanceof Error ? error : undefined
132
+ }, `[remote-desktop] auto-expire failed: ${error instanceof Error ? error.message : String(error)}`);
133
+ });
134
+ }, durationMs);
135
+ }
136
+ async function startTailscaleVncSession(args) {
137
+ const ts = await probeTailscale();
138
+ if (!ts.authenticated) {
139
+ throw new RemoteDesktopError("Tailscale is not authenticated", "tailscale-vnc");
140
+ }
141
+ const host = args.tailscaleNodeOverride || ts.hostname;
142
+ if (!host) {
143
+ throw new RemoteDesktopError("Tailscale hostname not discoverable", "tailscale-vnc");
144
+ }
145
+ if (process.platform === "darwin") {
146
+ const vncUp = await probeLocalVncServer();
147
+ if (!vncUp) {
148
+ throw new RemoteDesktopError("macOS Screen Sharing is not enabled. Enable it in System Settings → General → Sharing → Screen Sharing, then retry.", "tailscale-vnc");
149
+ }
150
+ }
151
+ return {
152
+ accessUrl: `vnc://${host}:${args.vncPort}`
153
+ };
154
+ }
155
+ async function startTailscaleSshSession(args) {
156
+ const ts = await probeTailscale();
157
+ if (!ts.authenticated) {
158
+ throw new RemoteDesktopError("Tailscale is not authenticated", "tailscale-ssh");
159
+ }
160
+ const host = args.tailscaleNodeOverride || ts.hostname;
161
+ if (!host) {
162
+ throw new RemoteDesktopError("Tailscale hostname not discoverable", "tailscale-ssh");
163
+ }
164
+ return { accessUrl: `ssh://${host}` };
165
+ }
166
+ async function startNgrokVncSession(args) {
167
+ const child = spawn("ngrok", ["tcp", String(args.vncPort), "--log=stdout", "--log-format=json"], {
168
+ env: { ...process.env, NGROK_AUTHTOKEN: args.authToken },
169
+ stdio: ["ignore", "pipe", "pipe"]
170
+ });
171
+ const accessUrl = await new Promise((resolve, reject) => {
172
+ const timer = setTimeout(() => {
173
+ reject(new RemoteDesktopError("ngrok did not report a public URL within 10s", "ngrok-vnc"));
174
+ }, 1e4);
175
+ const onStdout = (chunk) => {
176
+ const text = chunk.toString("utf8");
177
+ for (const line of text.split(`
178
+ `)) {
179
+ if (!line.trim())
180
+ continue;
181
+ try {
182
+ const evt = JSON.parse(line);
183
+ if (evt.url?.startsWith("tcp://")) {
184
+ clearTimeout(timer);
185
+ child.stdout.off("data", onStdout);
186
+ resolve(evt.url);
187
+ return;
188
+ }
189
+ } catch {}
190
+ }
191
+ };
192
+ child.stdout.on("data", onStdout);
193
+ child.once("error", (err) => {
194
+ clearTimeout(timer);
195
+ reject(new RemoteDesktopError(`ngrok failed: ${err.message}`, "ngrok-vnc"));
196
+ });
197
+ child.once("exit", (code) => {
198
+ clearTimeout(timer);
199
+ reject(new RemoteDesktopError(`ngrok exited prematurely with code ${code}`, "ngrok-vnc"));
200
+ });
201
+ });
202
+ return { accessUrl, child };
203
+ }
204
+ async function startRemoteSession(config) {
205
+ const resolved = resolveConfig(config);
206
+ const mockEnabled = isMockRemoteDesktopEnabled();
207
+ const backend = await detectRemoteDesktopBackend(config);
208
+ const now = new Date;
209
+ const durationMs = resolved.sessionDurationMinutes * 60000;
210
+ const id = randomUUID();
211
+ const pairingCode = generatePairingCode();
212
+ const initialSession = {
213
+ id,
214
+ backend,
215
+ status: "starting",
216
+ accessCode: pairingCode,
217
+ startedAt: now.toISOString(),
218
+ expiresAt: new Date(now.getTime() + durationMs).toISOString()
219
+ };
220
+ sessions.set(id, { session: initialSession });
221
+ if (backend === "none") {
222
+ const failed = {
223
+ ...initialSession,
224
+ status: "failed",
225
+ error: "No remote-desktop backend available. Configure Tailscale or ngrok.",
226
+ endedAt: new Date().toISOString()
227
+ };
228
+ sessions.set(id, { session: failed });
229
+ return failed;
230
+ }
231
+ try {
232
+ if (mockEnabled) {
233
+ const activeSession2 = {
234
+ ...initialSession,
235
+ backend,
236
+ status: "active",
237
+ accessUrl: `vnc://127.0.0.1:${resolved.vncPort}/mock/${id}`,
238
+ mockMode: true
239
+ };
240
+ const expiryTimer2 = scheduleExpiry(id, durationMs);
241
+ sessions.set(id, {
242
+ session: activeSession2,
243
+ expiryTimer: expiryTimer2
244
+ });
245
+ return activeSession2;
246
+ }
247
+ let accessUrl;
248
+ let ngrokProcess;
249
+ if (backend === "tailscale-vnc") {
250
+ const result = await startTailscaleVncSession({
251
+ id,
252
+ pairingCode,
253
+ vncPort: resolved.vncPort,
254
+ tailscaleNodeOverride: resolved.tailscaleNodeName
255
+ });
256
+ accessUrl = result.accessUrl;
257
+ } else if (backend === "tailscale-ssh") {
258
+ const result = await startTailscaleSshSession({
259
+ tailscaleNodeOverride: resolved.tailscaleNodeName
260
+ });
261
+ accessUrl = result.accessUrl;
262
+ } else {
263
+ if (!resolved.ngrokAuthToken) {
264
+ throw new RemoteDesktopError("ngrok auth token not configured (ELIZA_NGROK_AUTH_TOKEN)", "ngrok-vnc");
265
+ }
266
+ const result = await startNgrokVncSession({
267
+ vncPort: resolved.vncPort,
268
+ authToken: resolved.ngrokAuthToken
269
+ });
270
+ accessUrl = result.accessUrl;
271
+ ngrokProcess = result.child;
272
+ }
273
+ const activeSession = {
274
+ ...initialSession,
275
+ status: "active",
276
+ accessUrl
277
+ };
278
+ const expiryTimer = scheduleExpiry(id, durationMs);
279
+ sessions.set(id, {
280
+ session: activeSession,
281
+ expiryTimer,
282
+ ngrokProcess
283
+ });
284
+ logger.info({
285
+ boundary: "lifeops",
286
+ integration: "remote-desktop",
287
+ sessionId: id,
288
+ backend
289
+ }, `[remote-desktop] session ${id} active via ${backend}`);
290
+ return activeSession;
291
+ } catch (error) {
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ const failed = {
294
+ ...initialSession,
295
+ status: "failed",
296
+ error: message,
297
+ endedAt: new Date().toISOString()
298
+ };
299
+ sessions.set(id, { session: failed });
300
+ logger.warn({
301
+ boundary: "lifeops",
302
+ integration: "remote-desktop",
303
+ sessionId: id,
304
+ backend
305
+ }, `[remote-desktop] session ${id} failed: ${message}`);
306
+ return failed;
307
+ }
308
+ }
309
+ async function getSessionStatus(id) {
310
+ const entry = sessions.get(id);
311
+ return entry ? entry.session : null;
312
+ }
313
+ async function endRemoteSession(id) {
314
+ const entry = sessions.get(id);
315
+ if (!entry)
316
+ return;
317
+ if (entry.expiryTimer) {
318
+ clearTimeout(entry.expiryTimer);
319
+ }
320
+ if (entry.ngrokProcess && entry.ngrokProcess.exitCode === null) {
321
+ entry.ngrokProcess.kill("SIGTERM");
322
+ }
323
+ const ended = {
324
+ ...entry.session,
325
+ status: "ended",
326
+ endedAt: new Date().toISOString()
327
+ };
328
+ sessions.set(id, { session: ended });
329
+ logger.info({
330
+ boundary: "lifeops",
331
+ integration: "remote-desktop",
332
+ sessionId: id
333
+ }, `[remote-desktop] session ${id} ended`);
334
+ }
335
+ async function listActiveSessions() {
336
+ return Array.from(sessions.values()).map((entry) => entry.session).filter((session) => session.status === "active" || session.status === "starting");
337
+ }
338
+
339
+ // src/remote/remote-session-service.ts
340
+ import { randomUUID as randomUUID2 } from "node:crypto";
341
+ import fs from "node:fs";
342
+ import path from "node:path";
343
+ import { logger as logger2, resolveStateDir } from "@elizaos/core";
344
+
345
+ // src/remote/pairing-code.ts
346
+ import { randomInt as randomInt2 } from "node:crypto";
347
+ var PAIRING_CODE_TTL_MS = 5 * 60 * 1000;
348
+ var PAIRING_CODE_LENGTH = 6;
349
+
350
+ class PairingCodeStore {
351
+ ttlMs;
352
+ now;
353
+ entries = new Map;
354
+ constructor(options = {}) {
355
+ this.ttlMs = options.ttlMs ?? PAIRING_CODE_TTL_MS;
356
+ this.now = options.now ?? Date.now;
357
+ }
358
+ issue(subject) {
359
+ const issuedAt = this.now();
360
+ const entry = {
361
+ code: generatePairingCode2(),
362
+ issuedAt,
363
+ expiresAt: issuedAt + this.ttlMs
364
+ };
365
+ this.entries.set(subject, entry);
366
+ return entry;
367
+ }
368
+ consume(subject, code) {
369
+ const entry = this.entries.get(subject);
370
+ if (!entry)
371
+ return false;
372
+ const now = this.now();
373
+ if (entry.expiresAt <= now) {
374
+ this.entries.delete(subject);
375
+ return false;
376
+ }
377
+ if (entry.code !== code) {
378
+ return false;
379
+ }
380
+ this.entries.delete(subject);
381
+ return true;
382
+ }
383
+ peek(subject) {
384
+ const entry = this.entries.get(subject);
385
+ if (!entry)
386
+ return;
387
+ if (entry.expiresAt <= this.now()) {
388
+ this.entries.delete(subject);
389
+ return;
390
+ }
391
+ return entry;
392
+ }
393
+ clear(subject) {
394
+ if (subject) {
395
+ this.entries.delete(subject);
396
+ } else {
397
+ this.entries.clear();
398
+ }
399
+ }
400
+ }
401
+ function generatePairingCode2() {
402
+ return randomInt2(0, 10 ** PAIRING_CODE_LENGTH).toString().padStart(PAIRING_CODE_LENGTH, "0");
403
+ }
404
+
405
+ // src/remote/remote-session-service.ts
406
+ class RemoteSessionError extends Error {
407
+ code;
408
+ constructor(code, message) {
409
+ super(message);
410
+ this.name = "RemoteSessionError";
411
+ this.code = code;
412
+ }
413
+ }
414
+ var PAIRING_SUBJECT = "agent";
415
+ function defaultIsLocalMode() {
416
+ return process.env.ELIZA_REMOTE_LOCAL_MODE === "1";
417
+ }
418
+ var nullDataPlane = {
419
+ resolve: () => ({
420
+ ingressUrl: null,
421
+ reason: "data-plane-not-configured"
422
+ })
423
+ };
424
+ function defaultStoragePath() {
425
+ return path.join(resolveStateDir(), "lifeops", "remote-sessions.json");
426
+ }
427
+
428
+ class RemoteSessionService {
429
+ sessions = new Map;
430
+ pairingCodes;
431
+ dataPlane;
432
+ isLocalMode;
433
+ now;
434
+ log;
435
+ storagePath;
436
+ constructor(options = {}) {
437
+ this.pairingCodes = options.pairingCodes ?? new PairingCodeStore;
438
+ this.dataPlane = options.dataPlane ?? nullDataPlane;
439
+ this.isLocalMode = options.isLocalMode ?? defaultIsLocalMode;
440
+ this.now = options.now ?? (() => new Date);
441
+ this.log = options.logger ?? logger2;
442
+ this.storagePath = options.storagePath ?? defaultStoragePath();
443
+ this.loadSessions();
444
+ }
445
+ issuePairingCode() {
446
+ const entry = this.pairingCodes.issue(PAIRING_SUBJECT);
447
+ return {
448
+ code: entry.code,
449
+ expiresAt: new Date(entry.expiresAt).toISOString()
450
+ };
451
+ }
452
+ async startSession(params) {
453
+ if (!params.confirmed) {
454
+ throw new RemoteSessionError("NOT_CONFIRMED", "Remote sessions require explicit confirmation (confirmed: true).");
455
+ }
456
+ const requester = params.requesterIdentity.trim();
457
+ if (!requester) {
458
+ throw new RemoteSessionError("MISSING_REQUESTER", "requesterIdentity is required.");
459
+ }
460
+ const localMode = this.isLocalMode();
461
+ let status = "pending";
462
+ if (!localMode) {
463
+ const code = params.pairingCode?.trim();
464
+ if (!code) {
465
+ throw new RemoteSessionError("PAIRING_CODE_REQUIRED", "Pairing code required for non-local sessions. Issue one with issuePairingCode() first.");
466
+ }
467
+ const ok = this.pairingCodes.consume(PAIRING_SUBJECT, code);
468
+ if (!ok) {
469
+ const denied = this.recordSession({
470
+ requesterIdentity: requester,
471
+ status: "denied",
472
+ ingressUrl: null,
473
+ reason: null,
474
+ localMode
475
+ });
476
+ this.log.warn({ boundary: "remote", sessionId: denied.id, requester }, "[RemoteSessionService] pairing-code rejected");
477
+ return {
478
+ sessionId: denied.id,
479
+ status: "denied",
480
+ ingressUrl: null,
481
+ reason: null,
482
+ localMode
483
+ };
484
+ }
485
+ status = "active";
486
+ } else {
487
+ status = "active";
488
+ }
489
+ const resolved = await this.dataPlane.resolve({
490
+ sessionId: "pending",
491
+ requesterIdentity: requester,
492
+ localMode
493
+ });
494
+ const session = this.recordSession({
495
+ requesterIdentity: requester,
496
+ status,
497
+ ingressUrl: resolved.ingressUrl,
498
+ reason: resolved.reason,
499
+ localMode
500
+ });
501
+ if (resolved.ingressUrl === null) {
502
+ this.log.info({
503
+ boundary: "remote",
504
+ sessionId: session.id,
505
+ reason: resolved.reason,
506
+ localMode
507
+ }, `[RemoteSessionService] session ${session.id} active but no data plane ingress (reason=${resolved.reason ?? "unknown"})`);
508
+ } else {
509
+ this.log.info({
510
+ boundary: "remote",
511
+ sessionId: session.id,
512
+ localMode
513
+ }, `[RemoteSessionService] session ${session.id} active`);
514
+ }
515
+ return {
516
+ sessionId: session.id,
517
+ status,
518
+ ingressUrl: session.ingressUrl,
519
+ reason: session.reason,
520
+ localMode
521
+ };
522
+ }
523
+ async revokeSession(sessionId) {
524
+ const session = this.sessions.get(sessionId);
525
+ if (!session) {
526
+ throw new RemoteSessionError("SESSION_NOT_FOUND", `No session found with id ${sessionId}.`);
527
+ }
528
+ if (session.status === "revoked" || session.status === "denied") {
529
+ return;
530
+ }
531
+ const endedAt = this.now().toISOString();
532
+ const revoked = {
533
+ ...session,
534
+ status: "revoked",
535
+ updatedAt: endedAt,
536
+ endedAt
537
+ };
538
+ this.sessions.set(sessionId, revoked);
539
+ this.persistSessions();
540
+ this.log.info({ boundary: "remote", sessionId }, `[RemoteSessionService] session ${sessionId} revoked`);
541
+ }
542
+ async listActiveSessions() {
543
+ return Array.from(this.sessions.values()).filter((s) => s.status === "active" || s.status === "pending");
544
+ }
545
+ async getSession(sessionId) {
546
+ return this.sessions.get(sessionId);
547
+ }
548
+ recordSession(input) {
549
+ const id = randomUUID2();
550
+ const nowIso = this.now().toISOString();
551
+ const session = {
552
+ id,
553
+ requesterIdentity: input.requesterIdentity,
554
+ status: input.status,
555
+ ingressUrl: input.ingressUrl,
556
+ reason: input.reason,
557
+ localMode: input.localMode,
558
+ createdAt: nowIso,
559
+ updatedAt: nowIso,
560
+ endedAt: input.status === "denied" || input.status === "revoked" ? nowIso : null
561
+ };
562
+ this.sessions.set(id, session);
563
+ this.persistSessions();
564
+ return session;
565
+ }
566
+ loadSessions() {
567
+ if (!fs.existsSync(this.storagePath)) {
568
+ return;
569
+ }
570
+ try {
571
+ const parsed = JSON.parse(fs.readFileSync(this.storagePath, "utf8"));
572
+ if (!Array.isArray(parsed)) {
573
+ return;
574
+ }
575
+ for (const entry of parsed) {
576
+ if (!entry || typeof entry !== "object") {
577
+ continue;
578
+ }
579
+ const session = entry;
580
+ if (typeof session.id !== "string" || typeof session.requesterIdentity !== "string" || typeof session.status !== "string" || typeof session.localMode !== "boolean" || typeof session.createdAt !== "string" || typeof session.updatedAt !== "string") {
581
+ continue;
582
+ }
583
+ this.sessions.set(session.id, {
584
+ id: session.id,
585
+ requesterIdentity: session.requesterIdentity,
586
+ status: session.status === "active" || session.status === "pending" || session.status === "denied" || session.status === "revoked" ? session.status : "revoked",
587
+ ingressUrl: typeof session.ingressUrl === "string" ? session.ingressUrl : null,
588
+ reason: session.reason === "data-plane-not-configured" || session.reason === "local-mode-no-ingress" ? session.reason : null,
589
+ localMode: session.localMode,
590
+ createdAt: session.createdAt,
591
+ updatedAt: session.updatedAt,
592
+ endedAt: typeof session.endedAt === "string" ? session.endedAt : null
593
+ });
594
+ }
595
+ } catch {}
596
+ }
597
+ persistSessions() {
598
+ fs.mkdirSync(path.dirname(this.storagePath), { recursive: true });
599
+ fs.writeFileSync(this.storagePath, JSON.stringify(Array.from(this.sessions.values()), null, 2), {
600
+ encoding: "utf8",
601
+ mode: 384
602
+ });
603
+ }
604
+ }
605
+ var singleton;
606
+ function getRemoteSessionService() {
607
+ if (!singleton) {
608
+ singleton = new RemoteSessionService;
609
+ }
610
+ return singleton;
611
+ }
612
+ function __resetRemoteSessionServiceForTests() {
613
+ singleton = undefined;
614
+ }
615
+
616
+ // src/actions/remote-desktop.ts
617
+ var ACTION_NAME = "REMOTE_DESKTOP";
618
+ var SUBACTIONS = {
619
+ start: {
620
+ description: "Open remote-control session via RemoteSessionService. Requires confirmed:true. " + "Local ELIZA_REMOTE_LOCAL_MODE=1 skips pairingCode; cloud requires 6-digit pairingCode.",
621
+ descriptionCompressed: "open remote session confirmed-true 6-digit-pairing local-mode-skips",
622
+ required: ["confirmed"],
623
+ optional: ["pairingCode"]
624
+ },
625
+ status: {
626
+ description: "Lookup remote session by sessionId via stored backend.",
627
+ descriptionCompressed: "lookup remote session sessionId stored-session-backend",
628
+ required: ["sessionId"]
629
+ },
630
+ end: {
631
+ description: "Close remote session by sessionId via stored backend.",
632
+ descriptionCompressed: "close remote session sessionId stored-session-backend",
633
+ required: ["sessionId"]
634
+ },
635
+ list: {
636
+ description: "List active remote sessions via RemoteSessionService: ids, status, ingress URLs, local-mode hints.",
637
+ descriptionCompressed: "list active remote sessions ids+status+ingress+local-mode-hint",
638
+ required: []
639
+ },
640
+ revoke: {
641
+ description: "Revoke active remote session by sessionId via RemoteSessionService.",
642
+ descriptionCompressed: "revoke active remote session sessionId",
643
+ required: ["sessionId"]
644
+ }
645
+ };
646
+ function coerceString(value) {
647
+ if (typeof value !== "string")
648
+ return;
649
+ const trimmed = value.trim();
650
+ return trimmed.length > 0 ? trimmed : undefined;
651
+ }
652
+ function formatLegacySession(session) {
653
+ const lines = [
654
+ `Session ${session.id}`,
655
+ ` backend: ${session.backend}`,
656
+ ` status: ${session.status}`
657
+ ];
658
+ if (session.accessUrl)
659
+ lines.push(` url: ${session.accessUrl}`);
660
+ if (session.accessCode)
661
+ lines.push(` code: ${session.accessCode}`);
662
+ if (session.expiresAt)
663
+ lines.push(` expires: ${session.expiresAt}`);
664
+ if (session.error)
665
+ lines.push(` error: ${session.error}`);
666
+ return lines.join(`
667
+ `);
668
+ }
669
+ async function handleStart(runtime, message, params) {
670
+ const backend = await detectRemoteDesktopBackend();
671
+ const startPrompt = `Starting a remote desktop session will expose this machine to the network via ${backend}.`;
672
+ const decision = await requireConfirmation({
673
+ runtime,
674
+ message,
675
+ actionName: ACTION_NAME,
676
+ pendingKey: `remote-start:${backend}`,
677
+ prompt: startPrompt
678
+ });
679
+ if (decision.status !== "confirmed") {
680
+ return {
681
+ text: decision.status === "pending" ? `${startPrompt} Reply yes to confirm or no to cancel.` : "Remote desktop start cancelled.",
682
+ success: decision.status === "pending",
683
+ values: {
684
+ success: false,
685
+ error: decision.status === "pending" ? "CONFIRMATION_REQUIRED" : "CANCELLED",
686
+ requiresConfirmation: decision.status === "pending",
687
+ backend
688
+ },
689
+ data: {
690
+ actionName: ACTION_NAME,
691
+ subaction: "start",
692
+ requiresConfirmation: decision.status === "pending",
693
+ backend,
694
+ intent: params.intent ?? null
695
+ }
696
+ };
697
+ }
698
+ const requesterIdentity = coerceString(params.requesterIdentity) ?? String(message.entityId);
699
+ try {
700
+ const result = await getRemoteSessionService().startSession({
701
+ requesterIdentity,
702
+ pairingCode: coerceString(params.pairingCode),
703
+ confirmed: true
704
+ });
705
+ if (result.status === "denied") {
706
+ return {
707
+ text: "Pairing code was invalid or expired. Request a fresh code and retry.",
708
+ success: false,
709
+ values: {
710
+ success: false,
711
+ error: "PAIRING_DENIED",
712
+ sessionId: result.sessionId
713
+ },
714
+ data: { actionName: ACTION_NAME, subaction: "start", session: result }
715
+ };
716
+ }
717
+ if (result.ingressUrl === null) {
718
+ return {
719
+ text: `Remote session ${result.sessionId} is authorized but the data plane is not configured (${result.reason ?? "unknown"}). Configure Tailscale (T9b) or the Eliza Cloud tunnel to complete pixel transport.`,
720
+ success: false,
721
+ values: {
722
+ success: false,
723
+ error: "DATA_PLANE_NOT_CONFIGURED",
724
+ requiresConfirmation: true,
725
+ sessionId: result.sessionId,
726
+ status: result.status,
727
+ ingressUrl: null,
728
+ reason: result.reason,
729
+ localMode: result.localMode
730
+ },
731
+ data: {
732
+ actionName: ACTION_NAME,
733
+ subaction: "start",
734
+ requiresConfirmation: true,
735
+ session: result
736
+ }
737
+ };
738
+ }
739
+ return {
740
+ text: `Remote session ${result.sessionId} active. Connect via ${result.ingressUrl}.`,
741
+ success: true,
742
+ values: {
743
+ success: true,
744
+ sessionId: result.sessionId,
745
+ status: result.status,
746
+ ingressUrl: result.ingressUrl,
747
+ localMode: result.localMode
748
+ },
749
+ data: { actionName: ACTION_NAME, subaction: "start", session: result }
750
+ };
751
+ } catch (error) {
752
+ if (error instanceof RemoteSessionError) {
753
+ return {
754
+ text: error.message,
755
+ success: false,
756
+ values: { success: false, error: error.code },
757
+ data: { actionName: ACTION_NAME, subaction: "start" }
758
+ };
759
+ }
760
+ throw error;
761
+ }
762
+ }
763
+ async function handleStatus(params) {
764
+ const sessionId = coerceString(params.sessionId);
765
+ if (!sessionId) {
766
+ return {
767
+ text: "Missing sessionId.",
768
+ success: false,
769
+ values: { success: false, error: "MISSING_SESSION_ID" },
770
+ data: { actionName: ACTION_NAME, subaction: "status" }
771
+ };
772
+ }
773
+ const session = await getSessionStatus(sessionId);
774
+ if (!session) {
775
+ return {
776
+ text: `No session found with id ${sessionId}.`,
777
+ success: false,
778
+ values: { success: false, error: "SESSION_NOT_FOUND" },
779
+ data: { actionName: ACTION_NAME, subaction: "status", sessionId }
780
+ };
781
+ }
782
+ return {
783
+ text: formatLegacySession(session),
784
+ success: true,
785
+ values: { success: true, status: session.status },
786
+ data: { actionName: ACTION_NAME, subaction: "status", session }
787
+ };
788
+ }
789
+ async function handleEnd(params) {
790
+ const sessionId = coerceString(params.sessionId);
791
+ if (!sessionId) {
792
+ return {
793
+ text: "Missing sessionId.",
794
+ success: false,
795
+ values: { success: false, error: "MISSING_SESSION_ID" },
796
+ data: { actionName: ACTION_NAME, subaction: "end" }
797
+ };
798
+ }
799
+ const existing = await getSessionStatus(sessionId);
800
+ if (!existing) {
801
+ return {
802
+ text: `No session found with id ${sessionId}.`,
803
+ success: false,
804
+ values: { success: false, error: "SESSION_NOT_FOUND" },
805
+ data: { actionName: ACTION_NAME, subaction: "end", sessionId }
806
+ };
807
+ }
808
+ await endRemoteSession(sessionId);
809
+ return {
810
+ text: `Remote session ${sessionId} ended.`,
811
+ success: true,
812
+ values: { success: true, sessionId },
813
+ data: { actionName: ACTION_NAME, subaction: "end", sessionId }
814
+ };
815
+ }
816
+ async function handleList() {
817
+ const sessions2 = await getRemoteSessionService().listActiveSessions();
818
+ if (sessions2.length === 0) {
819
+ return {
820
+ text: "No active remote sessions.",
821
+ success: true,
822
+ values: { success: true, count: 0 },
823
+ data: { actionName: ACTION_NAME, subaction: "list", sessions: [] }
824
+ };
825
+ }
826
+ const lines = sessions2.map((s) => `• ${s.id} — status=${s.status}${s.ingressUrl ? ` ingress=${s.ingressUrl}` : ` ingress=<none:${s.reason ?? "unknown"}>`}${s.localMode ? " (local)" : ""}`);
827
+ return {
828
+ text: `Active remote sessions (${sessions2.length}):
829
+ ${lines.join(`
830
+ `)}`,
831
+ success: true,
832
+ values: { success: true, count: sessions2.length },
833
+ data: { actionName: ACTION_NAME, subaction: "list", sessions: sessions2 }
834
+ };
835
+ }
836
+ async function handleRevoke(params) {
837
+ const sessionId = coerceString(params.sessionId);
838
+ if (!sessionId) {
839
+ return {
840
+ text: "Missing sessionId.",
841
+ success: false,
842
+ values: { success: false, error: "MISSING_SESSION_ID" },
843
+ data: { actionName: ACTION_NAME, subaction: "revoke" }
844
+ };
845
+ }
846
+ try {
847
+ await getRemoteSessionService().revokeSession(sessionId);
848
+ return {
849
+ text: `Remote session ${sessionId} revoked.`,
850
+ success: true,
851
+ values: { success: true, sessionId },
852
+ data: { actionName: ACTION_NAME, subaction: "revoke", sessionId }
853
+ };
854
+ } catch (error) {
855
+ if (error instanceof RemoteSessionError) {
856
+ return {
857
+ text: error.message,
858
+ success: false,
859
+ values: { success: false, error: error.code, sessionId },
860
+ data: { actionName: ACTION_NAME, subaction: "revoke", sessionId }
861
+ };
862
+ }
863
+ throw error;
864
+ }
865
+ }
866
+ var remoteDesktopAction = {
867
+ name: ACTION_NAME,
868
+ similes: [
869
+ "REMOTE_SESSION",
870
+ "VNC_SESSION",
871
+ "REMOTE_CONTROL",
872
+ "PHONE_REMOTE_ACCESS",
873
+ "CONNECT_FROM_PHONE"
874
+ ],
875
+ description: "Remote-desktop sessions; owner connects to this machine from another device. " + "Subactions start confirmed:true cloud pairingCode; status|end|revoke sessionId; list active.",
876
+ descriptionCompressed: "REMOTE_DESKTOP start|status|end|list|revoke; start confirmed:true; cloud pairingCode",
877
+ tags: [
878
+ "domain:meta",
879
+ "capability:read",
880
+ "capability:write",
881
+ "capability:execute",
882
+ "capability:delete",
883
+ "surface:device",
884
+ "surface:internal",
885
+ "risk:irreversible"
886
+ ],
887
+ contexts: ["browser", "automation", "settings", "admin", "terminal"],
888
+ roleGate: { minRole: "OWNER" },
889
+ suppressPostActionContinuation: true,
890
+ validate: async () => true,
891
+ parameters: [
892
+ {
893
+ name: "action",
894
+ description: "start | status | end | list | revoke.",
895
+ descriptionCompressed: "remote-desktop action: start|status|end|list|revoke",
896
+ required: false,
897
+ schema: {
898
+ type: "string",
899
+ enum: ["start", "status", "end", "list", "revoke"]
900
+ },
901
+ examples: ["start", "list", "revoke"]
902
+ },
903
+ {
904
+ name: "sessionId",
905
+ description: "Session id. Required status|end|revoke.",
906
+ descriptionCompressed: "session id (status|end|revoke)",
907
+ required: false,
908
+ schema: { type: "string" },
909
+ examples: ["rs_abc123"]
910
+ },
911
+ {
912
+ name: "confirmed",
913
+ description: "true required for start; security gate.",
914
+ descriptionCompressed: "true required for start (security)",
915
+ required: false,
916
+ schema: { type: "boolean" }
917
+ },
918
+ {
919
+ name: "pairingCode",
920
+ description: "6-digit pairingCode for start. Required unless ELIZA_REMOTE_LOCAL_MODE=1.",
921
+ descriptionCompressed: "6-digit pairing code (start; skipped in local mode)",
922
+ required: false,
923
+ schema: { type: "string", pattern: "^[0-9]{6}$" },
924
+ examples: ["482193"]
925
+ },
926
+ {
927
+ name: "requesterIdentity",
928
+ description: "Requester id/name/device. Audit start.",
929
+ descriptionCompressed: "audit: requester id (start)",
930
+ required: false,
931
+ schema: { type: "string" }
932
+ },
933
+ {
934
+ name: "intent",
935
+ description: "Owner intent/reason. Audit.",
936
+ descriptionCompressed: "audit: owner reason",
937
+ required: false,
938
+ schema: { type: "string" }
939
+ }
940
+ ],
941
+ examples: [
942
+ [
943
+ {
944
+ name: "{{name1}}",
945
+ content: {
946
+ text: "Start a remote session with pairing code 482193, confirmed."
947
+ }
948
+ },
949
+ {
950
+ name: "{{agentName}}",
951
+ content: {
952
+ text: "Remote session active. Connect via vnc://host:5900.",
953
+ action: ACTION_NAME
954
+ }
955
+ }
956
+ ],
957
+ [
958
+ {
959
+ name: "{{name1}}",
960
+ content: { text: "Are any remote sessions open right now?" }
961
+ },
962
+ {
963
+ name: "{{agentName}}",
964
+ content: {
965
+ text: "No active remote sessions.",
966
+ action: ACTION_NAME
967
+ }
968
+ }
969
+ ],
970
+ [
971
+ {
972
+ name: "{{name1}}",
973
+ content: { text: "End the remote session rs_abc123." }
974
+ },
975
+ {
976
+ name: "{{agentName}}",
977
+ content: {
978
+ text: "Remote session rs_abc123 revoked.",
979
+ action: ACTION_NAME
980
+ }
981
+ }
982
+ ]
983
+ ],
984
+ handler: async (runtime, message, state, options) => {
985
+ const resolved = await resolveActionArgs({
986
+ runtime,
987
+ message,
988
+ ...state ? { state } : {},
989
+ ...options ? { options } : {},
990
+ actionName: ACTION_NAME,
991
+ subactions: SUBACTIONS
992
+ });
993
+ if (!resolved.ok) {
994
+ return {
995
+ success: false,
996
+ text: resolved.clarification,
997
+ values: {
998
+ success: false,
999
+ error: "MISSING_REMOTE_DESKTOP_ARGUMENTS",
1000
+ missing: resolved.missing
1001
+ },
1002
+ data: {
1003
+ actionName: ACTION_NAME,
1004
+ reason: "missing_arguments",
1005
+ missing: resolved.missing
1006
+ }
1007
+ };
1008
+ }
1009
+ const { subaction, params } = resolved;
1010
+ switch (subaction) {
1011
+ case "start":
1012
+ return handleStart(runtime, message, params);
1013
+ case "status":
1014
+ return handleStatus(params);
1015
+ case "end":
1016
+ return handleEnd(params);
1017
+ case "list":
1018
+ return handleList();
1019
+ case "revoke":
1020
+ return handleRevoke(params);
1021
+ }
1022
+ }
1023
+ };
1024
+ var REMOTE_DESKTOP_ACTION_NAME = ACTION_NAME;
1025
+
1026
+ // src/plugin.ts
1027
+ var remoteDesktopPlugin = {
1028
+ name: "remote-desktop",
1029
+ description: "Remote desktop session control for Eliza agents. REMOTE_DESKTOP umbrella action (start/status/end/list/revoke) over Tailscale VNC/SSH and ngrok backends with pairing-code confirmation. Extracted from @elizaos/plugin-personal-assistant.",
1030
+ actions: [remoteDesktopAction]
1031
+ };
1032
+
1033
+ // src/index.ts
1034
+ var src_default = remoteDesktopPlugin;
1035
+ export {
1036
+ startRemoteSession,
1037
+ remoteDesktopPlugin,
1038
+ remoteDesktopAction,
1039
+ listActiveSessions,
1040
+ getSessionStatus,
1041
+ getRemoteSessionService,
1042
+ generatePairingCode2 as generatePairingCode,
1043
+ endRemoteSession,
1044
+ detectRemoteDesktopBackend,
1045
+ src_default as default,
1046
+ __resetRemoteSessionServiceForTests,
1047
+ RemoteSessionService,
1048
+ RemoteSessionError,
1049
+ RemoteDesktopError,
1050
+ REMOTE_DESKTOP_ACTION_NAME,
1051
+ PairingCodeStore,
1052
+ PAIRING_CODE_TTL_MS,
1053
+ PAIRING_CODE_LENGTH
1054
+ };
1055
+
1056
+ //# debugId=0352AD5350E719D164756E2164756E21