@runuai/host 0.9.49 → 0.9.50

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.
@@ -0,0 +1,221 @@
1
+ /**
2
+ * ADR-117: one-shot engine connect over the bridge.
3
+ *
4
+ * Much simpler than engine-login: nothing is spawned in a container. With a
5
+ * `secret` this runs the SAME paste paths the local UI's connect uses
6
+ * (`connectEngine` with pastedToken/apiKey, or the ADR-116 labeled-account
7
+ * sink); without one it runs OpenCode's re-detect. The secret arrives in the
8
+ * start frame, goes straight into `connectEngine`/`addEngineAccount`, and is
9
+ * never logged or echoed into any event.
10
+ */
11
+
12
+ import {
13
+ isEngineAccountLabel,
14
+ isHostOpId,
15
+ MAX_ENGINE_CONNECT_LINE_CHARS,
16
+ MAX_ENGINE_CONNECT_SECRET_CHARS,
17
+ type EngineConnectEventFrame,
18
+ type EngineConnectKind,
19
+ } from "../src/protocol";
20
+ import { addEngineAccount } from "./engine-accounts";
21
+ import { connectEngine, type EngineKind } from "./engines";
22
+
23
+ const CONNECT_TIMEOUT_MS = 2 * 60_000;
24
+ const MAX_CONCURRENT_CONNECTS = 1;
25
+
26
+ export type EmitEngineConnectEvent = (frame: EngineConnectEventFrame) => void;
27
+
28
+ export interface EngineConnectManagerOptions {
29
+ /** Re-advertise capabilities after a successful mutation (main.ts). */
30
+ readvertise(): void;
31
+ /** Refresh running agents of a kind after a DEFAULT credential change. */
32
+ refreshEngineAccountAgents(kind: string): Promise<void>;
33
+ /** Best-effort post-connect image maintenance (optional engines bake in). */
34
+ ensureStandardImage(): Promise<unknown>;
35
+ timers?: {
36
+ setTimeout(handler: () => void, ms: number): NodeJS.Timeout;
37
+ clearTimeout(timer: NodeJS.Timeout): void;
38
+ };
39
+ }
40
+
41
+ interface ConnectOperation {
42
+ opId: string;
43
+ engine: EngineConnectKind;
44
+ emit: EmitEngineConnectEvent;
45
+ done: boolean;
46
+ timer: NodeJS.Timeout | null;
47
+ }
48
+
49
+ export class EngineConnectManager {
50
+ private readonly operations = new Map<string, ConnectOperation>();
51
+
52
+ constructor(private readonly options: EngineConnectManagerOptions) {}
53
+
54
+ start(
55
+ opId: string,
56
+ engine: EngineConnectKind,
57
+ secret: string | undefined,
58
+ label: string | undefined,
59
+ emit: EmitEngineConnectEvent,
60
+ ): boolean {
61
+ if (!isHostOpId(opId) || this.operations.has(opId)) return false;
62
+ if (
63
+ secret !== undefined &&
64
+ (secret.length < 1 || secret.length > MAX_ENGINE_CONNECT_SECRET_CHARS)
65
+ ) {
66
+ return false;
67
+ }
68
+ if (label !== undefined && !isEngineAccountLabel(label)) return false;
69
+ if (this.operations.size >= MAX_CONCURRENT_CONNECTS) {
70
+ safeEmit(emit, {
71
+ kind: "engine.connect.event",
72
+ opId,
73
+ engine,
74
+ phase: "done",
75
+ ok: false,
76
+ message: "Another engine connect is already in progress.",
77
+ });
78
+ return true;
79
+ }
80
+ const operation: ConnectOperation = {
81
+ opId,
82
+ engine,
83
+ emit,
84
+ done: false,
85
+ timer: null,
86
+ };
87
+ this.operations.set(opId, operation);
88
+ const timers = this.options.timers ?? {
89
+ setTimeout: (handler: () => void, ms: number) => setTimeout(handler, ms),
90
+ clearTimeout: (timer: NodeJS.Timeout) => clearTimeout(timer),
91
+ };
92
+ operation.timer = timers.setTimeout(() => {
93
+ this.finish(operation, false, "Engine connect timed out.");
94
+ }, CONNECT_TIMEOUT_MS);
95
+ operation.timer.unref?.();
96
+ void this.run(operation, secret, label).catch((error: unknown) => {
97
+ this.finish(
98
+ operation,
99
+ false,
100
+ error instanceof Error ? error.message : "Engine connect failed.",
101
+ );
102
+ });
103
+ return true;
104
+ }
105
+
106
+ stop(opId: string): void {
107
+ const operation = this.operations.get(opId);
108
+ if (operation) this.finish(operation, false, "Cancelled.");
109
+ }
110
+
111
+ stopAll(): void {
112
+ for (const operation of [...this.operations.values()]) {
113
+ this.finish(operation, false, "The connection was replaced.");
114
+ }
115
+ }
116
+
117
+ private async run(
118
+ operation: ConnectOperation,
119
+ secret: string | undefined,
120
+ label: string | undefined,
121
+ ): Promise<void> {
122
+ const { engine } = operation;
123
+ const emitLine = (line: string): void => {
124
+ const bounded = line.trim().slice(0, MAX_ENGINE_CONNECT_LINE_CHARS);
125
+ if (!bounded || operation.done) return;
126
+ safeEmit(operation.emit, {
127
+ kind: "engine.connect.event",
128
+ opId: operation.opId,
129
+ engine,
130
+ phase: "line",
131
+ line: bounded,
132
+ });
133
+ };
134
+
135
+ if (secret === undefined && engine !== "opencode") {
136
+ this.finish(
137
+ operation,
138
+ false,
139
+ "This engine needs a credential here, or its sign-in run on the host.",
140
+ );
141
+ return;
142
+ }
143
+ if (secret !== undefined && engine === "opencode") {
144
+ // saveApiKey has no opencode branch and would claim success without
145
+ // storing anything — OpenCode connects on the host, we only re-detect.
146
+ this.finish(
147
+ operation,
148
+ false,
149
+ "OpenCode signs in on the host (`opencode auth login`); use Check connection here.",
150
+ );
151
+ return;
152
+ }
153
+
154
+ // ADR-116 labeled extra account — the sink validates kind support.
155
+ if (label !== undefined && secret !== undefined) {
156
+ const added = addEngineAccount(engine, label, { token: secret });
157
+ if (added.ok) this.afterMutation(engine, false);
158
+ this.finish(operation, added.ok, added.message);
159
+ return;
160
+ }
161
+
162
+ const result = await connectEngine(
163
+ engine as EngineKind,
164
+ secret === undefined
165
+ ? {}
166
+ : engine === "claude"
167
+ ? { pastedToken: secret }
168
+ : { apiKey: secret },
169
+ emitLine,
170
+ );
171
+ if (result.ok) this.afterMutation(engine, true);
172
+ this.finish(operation, result.ok, result.message);
173
+ }
174
+
175
+ /** Mirror the local UI's post-connect behavior (ui/server.ts). */
176
+ private afterMutation(engine: EngineConnectKind, defaultSlot: boolean): void {
177
+ this.options.readvertise();
178
+ void this.options.ensureStandardImage().catch(() => {});
179
+ if (defaultSlot) {
180
+ void this.options.refreshEngineAccountAgents(engine).catch(() => {});
181
+ }
182
+ }
183
+
184
+ private finish(
185
+ operation: ConnectOperation,
186
+ ok: boolean,
187
+ message: string,
188
+ ): void {
189
+ if (operation.done) return;
190
+ operation.done = true;
191
+ if (operation.timer) {
192
+ (this.options.timers ?? { clearTimeout }).clearTimeout(operation.timer);
193
+ }
194
+ this.operations.delete(operation.opId);
195
+ safeEmit(operation.emit, {
196
+ kind: "engine.connect.event",
197
+ opId: operation.opId,
198
+ engine: operation.engine,
199
+ phase: "done",
200
+ ok,
201
+ message,
202
+ });
203
+ }
204
+ }
205
+
206
+ function safeEmit(
207
+ emit: EmitEngineConnectEvent,
208
+ frame: EngineConnectEventFrame,
209
+ ): void {
210
+ try {
211
+ emit(frame);
212
+ } catch {
213
+ // The bridge socket owns delivery failures.
214
+ }
215
+ }
216
+
217
+ export function createEngineConnectManager(
218
+ options: EngineConnectManagerOptions,
219
+ ): EngineConnectManager {
220
+ return new EngineConnectManager(options);
221
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.49",
3
+ "version": "0.9.50",
4
4
  "description": "Uai host — runs ephemeral AI tasks in containers on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Uai Tech <team@runuai.com>",
package/src/main.ts CHANGED
@@ -144,7 +144,13 @@ import {
144
144
  type HostConfigResult,
145
145
  } from "../lib/host-config";
146
146
  import { listEngineAccounts } from "../lib/engine-accounts";
147
+ import { createEngineConnectManager } from "../lib/engine-connect";
147
148
  import { createEngineLoginManager } from "../lib/engine-login";
149
+ import {
150
+ engineCatalog,
151
+ engineCliStatuses,
152
+ engineStatuses,
153
+ } from "../lib/engines";
148
154
  // Importing the real factory triggers the built-in adapters' register()
149
155
  // calls (claude, codex), so the registry is populated before we advertise.
150
156
  import "../lib/agents/factory";
@@ -166,12 +172,17 @@ import {
166
172
  HOST_MAINTENANCE_READINESS_PROTOCOL_FEATURE,
167
173
  HOST_CONFIG_PROTOCOL_FEATURE,
168
174
  HOST_ENGINE_ACCOUNTS_PROTOCOL_FEATURE,
175
+ HOST_ENGINE_CONNECT_PROTOCOL_FEATURE,
169
176
  HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
170
177
  HOST_LOGS_PROTOCOL_FEATURE,
171
178
  HOST_TASK_INVENTORY_PROTOCOL_FEATURE,
172
179
  MAX_ENGINE_ACCOUNT_LABEL_CHARS,
173
180
  MAX_ENGINE_ACCOUNTS_ADVERTISED,
181
+ MAX_ENGINE_CATALOG_ENTRIES,
182
+ isEngineConnectStartFrame,
183
+ isEngineConnectStopFrame,
174
184
  type EngineAccountSummary,
185
+ type EngineCatalogCapability,
175
186
  MAX_HOST_TASK_INVENTORY_PAGE_SIZE,
176
187
  MCP_GATEWAY_HEALTH_PROTOCOL_FEATURE,
177
188
  SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE,
@@ -268,6 +279,7 @@ function trackRemoteOperationCleanup(cleanup: Promise<void>): Promise<void> {
268
279
 
269
280
  function stopRemoteOperationManagers(): Promise<void> {
270
281
  hostLogManager.stopAll();
282
+ engineConnectManager.stopAll();
271
283
  return trackRemoteOperationCleanup(
272
284
  Promise.all([
273
285
  engineLoginManager.stopAll(),
@@ -427,6 +439,13 @@ const engineLoginManager = createEngineLoginManager({
427
439
  tempRoot: join(env.uaiHome, "engine-login-bind"),
428
440
  reconcileOnCreate: false,
429
441
  });
442
+ // ADR-117: one-shot connect (pasted key / OpenCode re-detect) over the bridge.
443
+ const engineConnectManager = createEngineConnectManager({
444
+ readvertise: () => sendCapabilities(),
445
+ refreshEngineAccountAgents: (engine) =>
446
+ getOrchestrator().refreshEngineAccountAgents(engine),
447
+ ensureStandardImage: () => ensureStandardImage(),
448
+ });
430
449
  if (initialRuntime.status === "ready") {
431
450
  console.log(
432
451
  `[host-agent] container runtime ready (${initialRuntime.provider}, ${initialRuntime.preference})`,
@@ -549,6 +568,7 @@ function buildCapabilities(): HostCapabilities {
549
568
  HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
550
569
  HOST_CONFIG_PROTOCOL_FEATURE,
551
570
  HOST_ENGINE_ACCOUNTS_PROTOCOL_FEATURE,
571
+ HOST_ENGINE_CONNECT_PROTOCOL_FEATURE,
552
572
  // The echo adapter cannot execute the in-task CLI. Advertising typed
553
573
  // dispatch in mock mode would let the composer create a Secretary that
554
574
  // has no way to wake crew.
@@ -565,10 +585,68 @@ function buildCapabilities(): HostCapabilities {
565
585
  mcpGateway: mcpGateway.state(),
566
586
  engineLogins: engineLoginManager.capabilities(),
567
587
  engineAccounts: engineAccountSummaries(),
588
+ engines: engineCatalogCapabilities(),
568
589
  githubUsers: connectedUserIds(),
569
590
  };
570
591
  }
571
592
 
593
+ // engineCliStatuses probes binaries asynchronously; capabilities build
594
+ // synchronously. The snapshot starts empty (cliInstalled: false) and each
595
+ // build kicks a refresh that re-advertises only when the answer changed.
596
+ let cliStatusSnapshot: Partial<Record<string, boolean>> = {};
597
+ let cliStatusRefreshing = false;
598
+ function refreshCliStatusSnapshot(): void {
599
+ if (cliStatusRefreshing) return;
600
+ cliStatusRefreshing = true;
601
+ void engineCliStatuses()
602
+ .then((next) => {
603
+ const changed =
604
+ JSON.stringify(next) !== JSON.stringify(cliStatusSnapshot);
605
+ cliStatusSnapshot = next;
606
+ if (changed) sendCapabilities();
607
+ })
608
+ .catch(() => {})
609
+ .finally(() => {
610
+ cliStatusRefreshing = false;
611
+ });
612
+ }
613
+
614
+ /** ADR-117: the full engine catalog for the cloud pane — descriptor metadata
615
+ * plus live connection/CLI state. The descriptor table stays authoritative. */
616
+ function engineCatalogCapabilities(): EngineCatalogCapability[] {
617
+ try {
618
+ const statuses = engineStatuses();
619
+ const cli = cliStatusSnapshot;
620
+ refreshCliStatusSnapshot();
621
+ return engineCatalog()
622
+ .slice(0, MAX_ENGINE_CATALOG_ENTRIES)
623
+ .map((entry): EngineCatalogCapability => {
624
+ const keyUrl = entry.apiKeyUrl ?? entry.getKeyUrl;
625
+ return {
626
+ kind: entry.kind,
627
+ label: entry.label,
628
+ connected: statuses[entry.kind] === true,
629
+ cliInstalled: cli[entry.kind] === true,
630
+ remote:
631
+ entry.kind === "kimi"
632
+ ? "none"
633
+ : entry.authMode === "external-login"
634
+ ? "detect"
635
+ : "secret",
636
+ ...(entry.apiKeyHint !== null ? { hint: entry.apiKeyHint } : {}),
637
+ ...(keyUrl !== null ? { keyUrl } : {}),
638
+ notes: entry.notes ?? "",
639
+ ...(entry.loginCmd !== null ? { loginCmd: entry.loginCmd } : {}),
640
+ };
641
+ });
642
+ } catch (error) {
643
+ console.warn(
644
+ `[host-agent] engine catalog for capabilities failed: ${error instanceof Error ? error.message : String(error)}`,
645
+ );
646
+ return [];
647
+ }
648
+ }
649
+
572
650
  /** ADR-116: the labeled-accounts advertisement — bounded, labels-only. */
573
651
  function engineAccountSummaries(): EngineAccountSummary[] {
574
652
  const summaries: EngineAccountSummary[] = [];
@@ -1118,6 +1196,18 @@ async function connect(): Promise<void> {
1118
1196
  engineLoginManager.stop(frame.opId).then(() => {}),
1119
1197
  );
1120
1198
  break;
1199
+ case "engine.connect.start":
1200
+ engineConnectManager.start(
1201
+ frame.opId,
1202
+ frame.engine,
1203
+ frame.secret,
1204
+ frame.label,
1205
+ (event) => sendRemoteOperationFrame(event),
1206
+ );
1207
+ break;
1208
+ case "engine.connect.stop":
1209
+ engineConnectManager.stop(frame.opId);
1210
+ break;
1121
1211
  case "host.config.get":
1122
1212
  sendHostConfigAck(
1123
1213
  sendRemoteOperationFrame,
@@ -2279,6 +2369,8 @@ function parseCloudFrame(data: RawData): CloudToHost | null {
2279
2369
  if (isEngineLoginInputFrame(frame)) return frame;
2280
2370
  if (isEngineLoginCallbackFrame(frame)) return frame;
2281
2371
  if (isEngineLoginStopFrame(frame)) return frame;
2372
+ if (isEngineConnectStartFrame(frame)) return frame;
2373
+ if (isEngineConnectStopFrame(frame)) return frame;
2282
2374
  if (isHostConfigGetFrame(frame)) return frame;
2283
2375
  if (isHostConfigSetFrame(frame)) return frame;
2284
2376
  if (
@@ -2589,6 +2681,16 @@ function rejectFrameDuringShutdown(
2589
2681
  message: error,
2590
2682
  });
2591
2683
  return true;
2684
+ case "engine.connect.start":
2685
+ send(socket, {
2686
+ kind: "engine.connect.event",
2687
+ opId: frame.opId,
2688
+ engine: frame.engine,
2689
+ phase: "done",
2690
+ ok: false,
2691
+ message: error,
2692
+ });
2693
+ return true;
2592
2694
  case "host.config.get":
2593
2695
  case "host.config.set":
2594
2696
  send(socket, {
@@ -2668,6 +2770,7 @@ function rejectFrameDuringShutdown(
2668
2770
  return true;
2669
2771
  case "host.logs.stop":
2670
2772
  case "engine.login.stop":
2773
+ case "engine.connect.stop":
2671
2774
  // Stops remain idempotent during drain and cannot admit new work.
2672
2775
  return false;
2673
2776
  case "engine.login.input":
package/src/protocol.ts CHANGED
@@ -92,6 +92,12 @@ export const HOST_CONFIG_PROTOCOL_FEATURE = "host-config-v1";
92
92
  export const HOST_ENGINE_ACCOUNTS_PROTOCOL_FEATURE = "host-engine-accounts-v1";
93
93
  export const MAX_ENGINE_ACCOUNT_LABEL_CHARS = 64;
94
94
  export const MAX_ENGINE_ACCOUNTS_ADVERTISED = 48;
95
+ /** ADR-117: one-shot engine credentials over the bridge + the full engine
96
+ * catalog in capabilities, so every engine renders on the cloud pane. */
97
+ export const HOST_ENGINE_CONNECT_PROTOCOL_FEATURE = "host-engine-connect-v1";
98
+ export const MAX_ENGINE_CONNECT_SECRET_CHARS = 8_192;
99
+ export const MAX_ENGINE_CONNECT_LINE_CHARS = 512;
100
+ export const MAX_ENGINE_CATALOG_ENTRIES = 8;
95
101
  export const MAX_HOST_OP_ID_CHARS = 128;
96
102
  export const MAX_HOST_LOG_LINES = 1_000;
97
103
  export const MAX_HOST_LOG_LINE_BYTES = 4 * 1_024;
@@ -363,6 +369,143 @@ export interface EngineAccountSummary {
363
369
  isDefault: boolean;
364
370
  }
365
371
 
372
+ /** ADR-117 engine kinds — every engine the host's descriptor table knows. */
373
+ export type EngineConnectKind =
374
+ | "claude"
375
+ | "codex"
376
+ | "kimi"
377
+ | "grok"
378
+ | "cursor"
379
+ | "opencode";
380
+
381
+ /** ADR-117: one catalog row per engine, advertised in capabilities so the
382
+ * cloud pane renders every engine from host truth — including the exact
383
+ * instruction copy for flows that only work on the host itself. */
384
+ export interface EngineCatalogCapability {
385
+ kind: EngineConnectKind;
386
+ label: string;
387
+ connected: boolean;
388
+ cliInstalled: boolean;
389
+ /** What the cloud can do remotely: submit a one-shot secret, re-detect a
390
+ * host-side login, or nothing (instructions only). */
391
+ remote: "secret" | "detect" | "none";
392
+ hint?: string;
393
+ keyUrl?: string;
394
+ notes: string;
395
+ loginCmd?: string;
396
+ }
397
+
398
+ /** ADR-117: start a one-shot connect. `secret` is the operator's pasted
399
+ * credential — the ONLY frame it ever rides; it is never echoed back,
400
+ * logged, or persisted cloud-side (ADR-100 amendment). */
401
+ export interface EngineConnectStartFrame {
402
+ kind: "engine.connect.start";
403
+ opId: HostOpId;
404
+ engine: EngineConnectKind;
405
+ secret?: string;
406
+ label?: string;
407
+ }
408
+
409
+ export interface EngineConnectStopFrame {
410
+ kind: "engine.connect.stop";
411
+ opId: HostOpId;
412
+ }
413
+
414
+ /** Secret-blind host→cloud connect progress: sanitized CLI/progress lines,
415
+ * then exactly one `done`. */
416
+ export interface EngineConnectEventFrame {
417
+ kind: "engine.connect.event";
418
+ opId: HostOpId;
419
+ engine: EngineConnectKind;
420
+ phase: "line" | "done";
421
+ line?: string;
422
+ ok?: boolean;
423
+ message?: string;
424
+ }
425
+
426
+ export function isEngineConnectKind(
427
+ value: unknown,
428
+ ): value is EngineConnectKind {
429
+ return (
430
+ value === "claude" ||
431
+ value === "codex" ||
432
+ value === "kimi" ||
433
+ value === "grok" ||
434
+ value === "cursor" ||
435
+ value === "opencode"
436
+ );
437
+ }
438
+
439
+ export function isEngineConnectStartFrame(
440
+ value: unknown,
441
+ ): value is EngineConnectStartFrame {
442
+ const frame = exactWireObject(
443
+ value,
444
+ ["kind", "opId", "engine"],
445
+ ["secret", "label"],
446
+ );
447
+ if (
448
+ !frame ||
449
+ frame.kind !== "engine.connect.start" ||
450
+ !isHostOpId(frame.opId) ||
451
+ !isEngineConnectKind(frame.engine)
452
+ ) {
453
+ return false;
454
+ }
455
+ if (frame.secret !== undefined) {
456
+ if (
457
+ typeof frame.secret !== "string" ||
458
+ frame.secret.length < 1 ||
459
+ frame.secret.length > MAX_ENGINE_CONNECT_SECRET_CHARS
460
+ ) {
461
+ return false;
462
+ }
463
+ }
464
+ return frame.label === undefined || isEngineAccountLabel(frame.label);
465
+ }
466
+
467
+ export function isEngineConnectStopFrame(
468
+ value: unknown,
469
+ ): value is EngineConnectStopFrame {
470
+ const frame = exactWireObject(value, ["kind", "opId"]);
471
+ return Boolean(
472
+ frame && frame.kind === "engine.connect.stop" && isHostOpId(frame.opId),
473
+ );
474
+ }
475
+
476
+ export function isEngineConnectEventFrame(
477
+ value: unknown,
478
+ ): value is EngineConnectEventFrame {
479
+ const frame = exactWireObject(
480
+ value,
481
+ ["kind", "opId", "engine", "phase"],
482
+ ["line", "ok", "message"],
483
+ );
484
+ if (
485
+ !frame ||
486
+ frame.kind !== "engine.connect.event" ||
487
+ !isHostOpId(frame.opId) ||
488
+ !isEngineConnectKind(frame.engine)
489
+ ) {
490
+ return false;
491
+ }
492
+ if (frame.phase === "line") {
493
+ return (
494
+ typeof frame.line === "string" &&
495
+ frame.line.length >= 1 &&
496
+ frame.line.length <= MAX_ENGINE_CONNECT_LINE_CHARS &&
497
+ frame.ok === undefined &&
498
+ frame.message === undefined
499
+ );
500
+ }
501
+ if (frame.phase !== "done") return false;
502
+ return (
503
+ typeof frame.ok === "boolean" &&
504
+ frame.line === undefined &&
505
+ (frame.message === undefined || isBoundedMessage(frame.message))
506
+ );
507
+ }
508
+
366
509
  /** The exact local host settings ADR-100 permits over the bridge. */
367
510
  export interface HostConfigState {
368
511
  agentCliAutoupdate: boolean;
@@ -803,6 +946,10 @@ export interface HostCapabilities {
803
946
  /** ADR-116: labeled engine accounts (default + extras), re-advertised on
804
947
  * every account mutation. Labels only — credentials never leave the host. */
805
948
  engineAccounts?: EngineAccountSummary[];
949
+ /** ADR-117: the full engine catalog — connection state plus the metadata
950
+ * the cloud pane needs to render every engine, including the instruction
951
+ * copy for host-only sign-in flows. */
952
+ engines?: EngineCatalogCapability[];
806
953
  // ADR-033: cloud user ids that currently have a GitHub token ON THIS HOST —
807
954
  // the per-host gh-connected state shown on the host detail page. Re-advertised
808
955
  // whenever a token is added/removed. Optional: older hosts omit it.
@@ -1320,6 +1467,8 @@ export type CloudToHost =
1320
1467
  | EngineLoginInputFrame
1321
1468
  | EngineLoginCallbackFrame
1322
1469
  | EngineLoginStopFrame
1470
+ | EngineConnectStartFrame
1471
+ | EngineConnectStopFrame
1323
1472
  | HostConfigGetFrame
1324
1473
  | HostConfigSetFrame
1325
1474
  | {
@@ -1470,6 +1619,7 @@ export type HostToCloud =
1470
1619
  | HostLogsChunkFrame
1471
1620
  | HostLogsEndFrame
1472
1621
  | EngineLoginEventFrame
1622
+ | EngineConnectEventFrame
1473
1623
  | HostConfigAckFrame;
1474
1624
 
1475
1625
  export type HostEvent =