@runuai/host 0.9.14 → 0.9.42

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 (97) hide show
  1. package/README.md +22 -5
  2. package/db/migrations/0014_host_inventory_event_index.sql +1 -0
  3. package/db/migrations/0015_host_settings.sql +9 -0
  4. package/db/migrations/0016_task_environment.sql +2 -0
  5. package/db/migrations/meta/_journal.json +21 -0
  6. package/db/schema.ts +80 -30
  7. package/images/standard/Dockerfile +36 -10
  8. package/images/standard/README.md +63 -18
  9. package/images/standard/container/corepack-version +1 -0
  10. package/images/standard/container/uai-init +308 -38
  11. package/images/standard/container/uai-materialize-runtimes +1527 -0
  12. package/lib/agent-cli.ts +33 -2
  13. package/lib/agent.ts +46 -7
  14. package/lib/agents/claude.ts +13 -8
  15. package/lib/agents/codex.ts +11 -6
  16. package/lib/agents/cursor.ts +39 -29
  17. package/lib/agents/durable-proc.ts +20 -27
  18. package/lib/agents/factory.ts +9 -25
  19. package/lib/agents/grok.ts +43 -30
  20. package/lib/agents/kimi.ts +44 -29
  21. package/lib/agents/opencode.ts +43 -31
  22. package/lib/agents/proc.ts +149 -114
  23. package/lib/agents/transport.ts +62 -50
  24. package/lib/agents/types.ts +6 -4
  25. package/lib/apple-runtime-recycle.ts +236 -0
  26. package/lib/apple-uninstall-teardown.ts +224 -0
  27. package/lib/browser-testing.ts +233 -93
  28. package/lib/codex-auth.ts +40 -6
  29. package/lib/command-db.ts +20 -0
  30. package/lib/container-runtime.ts +1338 -0
  31. package/lib/db.ts +1 -0
  32. package/lib/docker-exec.ts +87 -5
  33. package/lib/engine-accounts.ts +68 -5
  34. package/lib/engine-login.ts +1952 -0
  35. package/lib/enrollment-state.ts +251 -0
  36. package/lib/env-file.ts +155 -0
  37. package/lib/env.ts +4 -0
  38. package/lib/git-diff.ts +98 -32
  39. package/lib/git-identity.ts +199 -87
  40. package/lib/github-tokens.ts +202 -91
  41. package/lib/host-cloud-url.ts +62 -0
  42. package/lib/host-config.ts +279 -0
  43. package/lib/host-logs.ts +962 -0
  44. package/lib/keyed-promise-tail.ts +23 -0
  45. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  46. package/lib/managed-activation-watcher.ts +72 -0
  47. package/lib/managed-install-owner-watcher.ts +55 -0
  48. package/lib/managed-operation-drain.ts +49 -0
  49. package/lib/managed-runtime.ts +3644 -0
  50. package/lib/managed-update-scheduler.ts +125 -0
  51. package/lib/mcp-gateway.ts +450 -23
  52. package/lib/orchestrator.ts +3060 -218
  53. package/lib/preview-sidecar.ts +57 -13
  54. package/lib/release-manifest.ts +708 -0
  55. package/lib/release-trust.ts +28 -0
  56. package/lib/runtime-activation-tail.ts +232 -0
  57. package/lib/runtime-archive.ts +1086 -0
  58. package/lib/runtime-authority.ts +79 -0
  59. package/lib/runtime-guard.ts +36 -0
  60. package/lib/runtime-provider-state.ts +169 -0
  61. package/lib/runtime-state.ts +232 -12
  62. package/lib/skills.ts +24 -3
  63. package/lib/ssh.ts +18 -0
  64. package/lib/standard-image.ts +1104 -141
  65. package/lib/stopped-task-status-queue.ts +44 -0
  66. package/lib/task-container-cli.ts +269 -0
  67. package/lib/task-diff.ts +66 -46
  68. package/lib/task-environment/apple-container.ts +757 -0
  69. package/lib/task-environment/docker.ts +945 -0
  70. package/lib/task-environment/index.ts +364 -0
  71. package/lib/task-environment/legacy-adoption.ts +443 -0
  72. package/lib/task-environment/registry.ts +58 -0
  73. package/lib/task-environment/types.ts +408 -0
  74. package/lib/task-identity.ts +19 -0
  75. package/lib/task-inventory.ts +585 -0
  76. package/lib/tunnel-registry.ts +135 -19
  77. package/lib/tunnel-runtime.ts +235 -0
  78. package/package.json +1 -1
  79. package/scripts/agent/_common.sh +123 -3
  80. package/scripts/agent/task-down.sh +146 -38
  81. package/scripts/agent/task-status.sh +19 -3
  82. package/scripts/agent/task-up.sh +1405 -107
  83. package/scripts/install/darwin.ts +848 -50
  84. package/scripts/install/linux.ts +838 -35
  85. package/scripts/install/types.ts +43 -0
  86. package/scripts/install/util.ts +215 -8
  87. package/scripts/install/win.ts +12 -0
  88. package/src/apple-tunnel-route.ts +104 -0
  89. package/src/cli.ts +1464 -72
  90. package/src/event-outbox.ts +83 -4
  91. package/src/index.ts +766 -42
  92. package/src/main.ts +1398 -255
  93. package/src/paths.ts +17 -1
  94. package/src/protocol.ts +695 -1
  95. package/src/runtime-bootstrap.ts +165 -0
  96. package/src/ui/server.ts +46 -10
  97. package/src/ui/types.ts +37 -0
package/src/protocol.ts CHANGED
@@ -29,6 +29,31 @@ export type HostCommandResult<T> =
29
29
  code: HostErrorCode;
30
30
  message: string;
31
31
  retryable?: boolean;
32
+ /**
33
+ * Narrow rolling-safe reason for a retryable refusal. Old cloud readers
34
+ * ignore this additive field; new readers use it only to bridge the
35
+ * short supervisor restart after a managed-runtime activation. Generic
36
+ * `retryable` failures (for example, no Docker runtime) remain fail-fast.
37
+ */
38
+ retryReason?: "managed_restart" | "runtime_setup";
39
+ /**
40
+ * Who decided this failed.
41
+ *
42
+ * `"cloud"` means the command never reached a host at all — the bridge
43
+ * had no socket, the socket belongs to another instance, or a relay/ack
44
+ * deadline expired. Absent means the HOST answered.
45
+ *
46
+ * It is a separate field rather than a distinct error code on purpose.
47
+ * `host_unavailable` is emitted from both sides — the host uses it for
48
+ * its own probes (Docker not answering, Git state unverifiable) — and a
49
+ * caller that must tell those apart, like the ADR-084 failure streak,
50
+ * cannot do it from the code. Splitting the CODE instead would put a new
51
+ * wire value in front of instances that predate it: during a rolling
52
+ * deploy an old cloud receives a relayed reply from a new socket owner,
53
+ * finds no case for it in `hostStatus`, and renders an error as HTTP 200.
54
+ * An unknown FIELD is ignored by old readers; an unknown CODE is not.
55
+ */
56
+ origin?: "cloud";
32
57
  };
33
58
 
34
59
  /** Capability ids shared by host advertisement and fail-closed cloud gates. */
@@ -46,6 +71,41 @@ export const GITHUB_CREDENTIAL_GENERATION_PROTOCOL_FEATURE =
46
71
  // a disconnect no longer loses events (they replay), so it keeps its
47
72
  // in-flight turn buffers across the gap.
48
73
  export const EVENT_REPLAY_PROTOCOL_FEATURE = "event-replay-v1";
74
+ /** ADR-100 L2: the cloud can request a bounded, positive-evidence inventory
75
+ * of task-owned state from this authenticated host generation. */
76
+ export const HOST_TASK_INVENTORY_PROTOCOL_FEATURE = "host-task-inventory-v1";
77
+ /** ADR-100 L3: a host advertising this feature publishes its one-way
78
+ * boot-maintenance readiness latch and rejects task/container work until the
79
+ * initial standard-image and shared-CLI reconciliation has settled. */
80
+ export const HOST_MAINTENANCE_READINESS_PROTOCOL_FEATURE =
81
+ "host-maintenance-readiness-v1";
82
+ /** ADR-100 L3: bounded health for the host-local MCP gateway listener. */
83
+ export const MCP_GATEWAY_HEALTH_PROTOCOL_FEATURE = "mcp-gateway-health-v1";
84
+ /** ADR-100 remote host operations. Each family is independently advertised so
85
+ * a rolling cloud can expose only controls that the connected host supports. */
86
+ export const HOST_LOGS_PROTOCOL_FEATURE = "host-logs-v1";
87
+ export const HOST_ENGINE_LOGIN_PROTOCOL_FEATURE = "host-engine-login-v1";
88
+ export const HOST_CONFIG_PROTOCOL_FEATURE = "host-config-v1";
89
+ export const MAX_HOST_OP_ID_CHARS = 128;
90
+ export const MAX_HOST_LOG_LINES = 1_000;
91
+ export const MAX_HOST_LOG_LINE_BYTES = 4 * 1_024;
92
+ export const MAX_HOST_LOG_CHUNK_LINES = 100;
93
+ export const MAX_HOST_LOG_CHUNK_BYTES = 64 * 1_024;
94
+ export const MAX_ENGINE_LOGIN_TEXT_CHARS = 4_096;
95
+ export const MAX_ENGINE_LOGIN_QUERY_CHARS = 4_096;
96
+ export const MAX_ENGINE_LOGIN_URL_CHARS = 2_048;
97
+ export const MAX_REMOTE_HOST_MESSAGE_BYTES = 512;
98
+ export const MAX_HOST_TASK_INVENTORY_PAGE_SIZE = 256;
99
+ export const MAX_HOST_TASK_INVENTORY_SCAN_ID_CHARS = 128;
100
+ export const MAX_HOST_TASK_INVENTORY_TASK_ID_CHARS = 128;
101
+ /**
102
+ * A redeem response carrying this exact header proves the cloud checks a spent
103
+ * token before expiry. That lets a new CLI distinguish a definitive 404/410
104
+ * from an old/rolled-back handler whose 410 could follow a committed response
105
+ * that was lost in transit.
106
+ */
107
+ export const ENROLLMENT_REPLAY_PROTOCOL_HEADER = "x-uai-enrollment-replay";
108
+ export const ENROLLMENT_REPLAY_PROTOCOL_VERSION = "same-host-v1";
49
109
  export const COMMUNICATOR_EXECUTION_PROFILE = "communicator";
50
110
  export const MAX_AGENT_ID_CHARS = 128;
51
111
  export const MAX_SECRETARY_DISPATCH_RECIPIENTS = 16;
@@ -68,6 +128,20 @@ export interface CommandContext {
68
128
  * dead resolver per retry.
69
129
  */
70
130
  timeoutMs?: number;
131
+ /**
132
+ * Cloud-internal routing override for a durable taskDown. Lifecycle cleanup
133
+ * may outlive the task row whose host assignment normally routes commands.
134
+ * This field is context metadata: it is never serialized onto the host
135
+ * command frame, and no host/request payload may populate it.
136
+ */
137
+ targetHostId?: string;
138
+ /**
139
+ * Cloud-internal credential-generation fence paired with targetHostId.
140
+ * Durable cleanup snapshots the registry verifier before source deletion;
141
+ * the bridge must route only to a socket authenticated with this exact hash.
142
+ * Like targetHostId, this is relay metadata and never reaches the host wire.
143
+ */
144
+ expectedHostTokenHash?: string;
71
145
  }
72
146
 
73
147
  export type PermissionDecision = { kind: "accept" } | { kind: "decline" };
@@ -100,6 +174,557 @@ export interface TaskAgent {
100
174
  permissions?: string[];
101
175
  }
102
176
 
177
+ export type ContainerRuntimePreference = "auto" | "docker" | "apple-container";
178
+
179
+ /** ADR-101 runtime state. The wire shape is deliberately impossible to
180
+ * misinterpret: transitional state has no verdict, ready names the selected
181
+ * provider, and a failed verdict always carries bounded operator guidance. */
182
+ export type ContainerRuntimeCapability =
183
+ | {
184
+ status: "checking";
185
+ preference: ContainerRuntimePreference;
186
+ provider?: never;
187
+ message?: never;
188
+ action?: never;
189
+ }
190
+ | {
191
+ status: "ready";
192
+ preference: "auto" | "docker";
193
+ provider: "docker";
194
+ message?: never;
195
+ action?: never;
196
+ }
197
+ | {
198
+ status: "ready";
199
+ preference: "auto" | "apple-container";
200
+ provider: "apple-container";
201
+ message?: never;
202
+ action?: never;
203
+ }
204
+ | {
205
+ status: "no-runtime";
206
+ preference: ContainerRuntimePreference;
207
+ provider?: never;
208
+ message: string;
209
+ action: string;
210
+ };
211
+
212
+ /** ADR-100: bounded operator-visible state for the host-local MCP gateway.
213
+ * Raw bind errors are deliberately not part of the capability frame. */
214
+ export type McpGatewayCapability =
215
+ | { status: "starting"; port: number }
216
+ | { status: "ready"; port: number }
217
+ | {
218
+ status: "down";
219
+ port: number;
220
+ reason: "port_in_use" | "bind_failed";
221
+ };
222
+
223
+ /** ADR-100 operation ids are deliberately URL/DOM-safe and bounded before
224
+ * they are admitted to any per-connection operation map. */
225
+ export type HostOpId = string;
226
+
227
+ export interface HostLogsStartFrame {
228
+ kind: "host.logs.start";
229
+ opId: HostOpId;
230
+ lines: number;
231
+ follow: boolean;
232
+ }
233
+
234
+ export interface HostLogsStopFrame {
235
+ kind: "host.logs.stop";
236
+ opId: HostOpId;
237
+ }
238
+
239
+ export interface HostLogsChunkFrame {
240
+ kind: "host.logs.chunk";
241
+ opId: HostOpId;
242
+ lines: string[];
243
+ /** Lines discarded by the host's bounded drop-oldest buffer. Omitted at 0. */
244
+ dropped?: number;
245
+ }
246
+
247
+ export type HostLogsEndReason = "complete" | "stopped" | "error";
248
+
249
+ export type HostLogsEndFrame =
250
+ | {
251
+ kind: "host.logs.end";
252
+ opId: HostOpId;
253
+ reason: "complete" | "stopped";
254
+ message?: never;
255
+ }
256
+ | {
257
+ kind: "host.logs.end";
258
+ opId: HostOpId;
259
+ reason: "error";
260
+ message: string;
261
+ };
262
+
263
+ export type EngineLoginKind = "claude" | "codex";
264
+ export type EngineLoginPhase =
265
+ | "starting"
266
+ | "authorize"
267
+ | "awaiting_input"
268
+ | "awaiting_callback"
269
+ | "succeeded"
270
+ | "failed"
271
+ | "cancelled"
272
+ | "timed_out";
273
+ export type EngineLoginErrorCode =
274
+ | "unavailable"
275
+ | "start_failed"
276
+ | "invalid_input"
277
+ | "invalid_callback"
278
+ | "persist_failed"
279
+ | "timeout";
280
+
281
+ export interface EngineLoginStartFrame {
282
+ kind: "engine.login.start";
283
+ opId: HostOpId;
284
+ engine: EngineLoginKind;
285
+ }
286
+
287
+ /** The only generic one-time response channel. Long-lived tokens must never
288
+ * be represented by this protocol and are persisted entirely on the host. */
289
+ export interface EngineLoginInputFrame {
290
+ kind: "engine.login.input";
291
+ opId: HostOpId;
292
+ text: string;
293
+ }
294
+
295
+ /** The pasted failed-localhost callback query used by Codex's login flow. */
296
+ export interface EngineLoginCallbackFrame {
297
+ kind: "engine.login.callback";
298
+ opId: HostOpId;
299
+ query: string;
300
+ }
301
+
302
+ export interface EngineLoginStopFrame {
303
+ kind: "engine.login.stop";
304
+ opId: HostOpId;
305
+ }
306
+
307
+ interface EngineLoginEventBase {
308
+ kind: "engine.login.event";
309
+ opId: HostOpId;
310
+ engine: EngineLoginKind;
311
+ message?: string;
312
+ }
313
+
314
+ /** Secret-blind host→cloud login progress. OAuth codes and callback queries
315
+ * have no field in this direction; `errorCode` is a closed machine reason. */
316
+ export type EngineLoginEventFrame =
317
+ | (EngineLoginEventBase & {
318
+ phase: "authorize";
319
+ url: string;
320
+ errorCode?: never;
321
+ })
322
+ | (EngineLoginEventBase & {
323
+ phase: "failed" | "timed_out";
324
+ url?: never;
325
+ errorCode?: EngineLoginErrorCode;
326
+ })
327
+ | (EngineLoginEventBase & {
328
+ phase:
329
+ | "starting"
330
+ | "awaiting_input"
331
+ | "awaiting_callback"
332
+ | "succeeded"
333
+ | "cancelled";
334
+ url?: never;
335
+ errorCode?: never;
336
+ });
337
+
338
+ /** Rolling-safe, secret-free engine state. v1 intentionally makes no claim
339
+ * about expiry; that requires positive adapter evidence in a future version. */
340
+ export interface EngineLoginCapability {
341
+ engine: EngineLoginKind;
342
+ status: "configured" | "missing";
343
+ loginSupported: true;
344
+ }
345
+
346
+ /** The exact local host settings ADR-100 permits over the bridge. */
347
+ export interface HostConfigState {
348
+ agentCliAutoupdate: boolean;
349
+ telemetryEnabled: boolean;
350
+ retainTerminalHistory: boolean;
351
+ }
352
+
353
+ export type HostConfigPatch = Partial<HostConfigState>;
354
+
355
+ export interface HostConfigGetFrame {
356
+ kind: "host.config.get";
357
+ opId: HostOpId;
358
+ }
359
+
360
+ export interface HostConfigSetFrame {
361
+ kind: "host.config.set";
362
+ opId: HostOpId;
363
+ patch: HostConfigPatch;
364
+ }
365
+
366
+ export type HostConfigErrorCode = "invalid_patch" | "persistence_failed";
367
+
368
+ export type HostConfigAckFrame =
369
+ | {
370
+ kind: "host.config.ack";
371
+ opId: HostOpId;
372
+ ok: true;
373
+ config: HostConfigState;
374
+ restartRequired: boolean;
375
+ }
376
+ | {
377
+ kind: "host.config.ack";
378
+ opId: HostOpId;
379
+ ok: false;
380
+ errorCode: HostConfigErrorCode;
381
+ message: string;
382
+ };
383
+
384
+ type WireObject = Record<string, unknown>;
385
+ const utf8Encoder = new TextEncoder();
386
+
387
+ function utf8ByteLength(value: string): number {
388
+ return utf8Encoder.encode(value).byteLength;
389
+ }
390
+
391
+ function exactWireObject(
392
+ value: unknown,
393
+ required: readonly string[],
394
+ optional: readonly string[] = [],
395
+ ): WireObject | null {
396
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
397
+ const object = value as WireObject;
398
+ const keys = Object.keys(object);
399
+ const allowed = new Set([...required, ...optional]);
400
+ if (
401
+ required.some((key) => !Object.hasOwn(object, key)) ||
402
+ keys.some((key) => !allowed.has(key))
403
+ ) {
404
+ return null;
405
+ }
406
+ return object;
407
+ }
408
+
409
+ function isBoundedMessage(value: unknown): value is string {
410
+ return (
411
+ typeof value === "string" &&
412
+ utf8ByteLength(value) <= MAX_REMOTE_HOST_MESSAGE_BYTES
413
+ );
414
+ }
415
+
416
+ function isEngineLoginKind(value: unknown): value is EngineLoginKind {
417
+ return value === "claude" || value === "codex";
418
+ }
419
+
420
+ function isEngineLoginErrorCode(value: unknown): value is EngineLoginErrorCode {
421
+ return (
422
+ value === "unavailable" ||
423
+ value === "start_failed" ||
424
+ value === "invalid_input" ||
425
+ value === "invalid_callback" ||
426
+ value === "persist_failed" ||
427
+ value === "timeout"
428
+ );
429
+ }
430
+
431
+ function isHttpsUrl(value: unknown): value is string {
432
+ if (
433
+ typeof value !== "string" ||
434
+ value.length === 0 ||
435
+ value.length > MAX_ENGINE_LOGIN_URL_CHARS ||
436
+ value.trim() !== value
437
+ ) {
438
+ return false;
439
+ }
440
+ try {
441
+ const url = new URL(value);
442
+ return url.protocol === "https:" && !url.username && !url.password;
443
+ } catch {
444
+ return false;
445
+ }
446
+ }
447
+
448
+ export function isHostOpId(value: unknown): value is HostOpId {
449
+ return (
450
+ typeof value === "string" &&
451
+ value.length <= MAX_HOST_OP_ID_CHARS &&
452
+ /^[A-Za-z0-9_-]+$/.test(value)
453
+ );
454
+ }
455
+
456
+ export function isHostLogsStartFrame(
457
+ value: unknown,
458
+ ): value is HostLogsStartFrame {
459
+ const frame = exactWireObject(value, ["kind", "opId", "lines", "follow"]);
460
+ return Boolean(
461
+ frame &&
462
+ frame.kind === "host.logs.start" &&
463
+ isHostOpId(frame.opId) &&
464
+ typeof frame.lines === "number" &&
465
+ Number.isSafeInteger(frame.lines) &&
466
+ frame.lines >= 1 &&
467
+ frame.lines <= MAX_HOST_LOG_LINES &&
468
+ typeof frame.follow === "boolean",
469
+ );
470
+ }
471
+
472
+ export function isHostLogsStopFrame(
473
+ value: unknown,
474
+ ): value is HostLogsStopFrame {
475
+ const frame = exactWireObject(value, ["kind", "opId"]);
476
+ return Boolean(
477
+ frame && frame.kind === "host.logs.stop" && isHostOpId(frame.opId),
478
+ );
479
+ }
480
+
481
+ export function isHostLogsChunkFrame(
482
+ value: unknown,
483
+ ): value is HostLogsChunkFrame {
484
+ const frame = exactWireObject(value, ["kind", "opId", "lines"], ["dropped"]);
485
+ if (
486
+ !frame ||
487
+ frame.kind !== "host.logs.chunk" ||
488
+ !isHostOpId(frame.opId) ||
489
+ !Array.isArray(frame.lines) ||
490
+ frame.lines.length < 1 ||
491
+ frame.lines.length > MAX_HOST_LOG_CHUNK_LINES ||
492
+ frame.lines.some(
493
+ (line) =>
494
+ typeof line !== "string" ||
495
+ utf8ByteLength(line) > MAX_HOST_LOG_LINE_BYTES,
496
+ ) ||
497
+ frame.lines.reduce(
498
+ (bytes, line) => bytes + utf8ByteLength(line as string),
499
+ 0,
500
+ ) > MAX_HOST_LOG_CHUNK_BYTES
501
+ ) {
502
+ return false;
503
+ }
504
+ return (
505
+ !Object.hasOwn(frame, "dropped") ||
506
+ (typeof frame.dropped === "number" &&
507
+ Number.isSafeInteger(frame.dropped) &&
508
+ frame.dropped >= 0)
509
+ );
510
+ }
511
+
512
+ export function isHostLogsEndFrame(value: unknown): value is HostLogsEndFrame {
513
+ const frame = exactWireObject(value, ["kind", "opId", "reason"], ["message"]);
514
+ if (
515
+ !frame ||
516
+ frame.kind !== "host.logs.end" ||
517
+ !isHostOpId(frame.opId)
518
+ ) {
519
+ return false;
520
+ }
521
+ if (frame.reason === "error") {
522
+ return Object.hasOwn(frame, "message") && isBoundedMessage(frame.message);
523
+ }
524
+ return (
525
+ (frame.reason === "complete" || frame.reason === "stopped") &&
526
+ !Object.hasOwn(frame, "message")
527
+ );
528
+ }
529
+
530
+ export function isEngineLoginStartFrame(
531
+ value: unknown,
532
+ ): value is EngineLoginStartFrame {
533
+ const frame = exactWireObject(value, ["kind", "opId", "engine"]);
534
+ return Boolean(
535
+ frame &&
536
+ frame.kind === "engine.login.start" &&
537
+ isHostOpId(frame.opId) &&
538
+ isEngineLoginKind(frame.engine),
539
+ );
540
+ }
541
+
542
+ export function isEngineLoginInputFrame(
543
+ value: unknown,
544
+ ): value is EngineLoginInputFrame {
545
+ const frame = exactWireObject(value, ["kind", "opId", "text"]);
546
+ return Boolean(
547
+ frame &&
548
+ frame.kind === "engine.login.input" &&
549
+ isHostOpId(frame.opId) &&
550
+ typeof frame.text === "string" &&
551
+ frame.text.length >= 1 &&
552
+ frame.text.length <= MAX_ENGINE_LOGIN_TEXT_CHARS,
553
+ );
554
+ }
555
+
556
+ export function isEngineLoginCallbackFrame(
557
+ value: unknown,
558
+ ): value is EngineLoginCallbackFrame {
559
+ const frame = exactWireObject(value, ["kind", "opId", "query"]);
560
+ return Boolean(
561
+ frame &&
562
+ frame.kind === "engine.login.callback" &&
563
+ isHostOpId(frame.opId) &&
564
+ typeof frame.query === "string" &&
565
+ frame.query.length >= 1 &&
566
+ frame.query.length <= MAX_ENGINE_LOGIN_QUERY_CHARS,
567
+ );
568
+ }
569
+
570
+ export function isEngineLoginStopFrame(
571
+ value: unknown,
572
+ ): value is EngineLoginStopFrame {
573
+ const frame = exactWireObject(value, ["kind", "opId"]);
574
+ return Boolean(
575
+ frame && frame.kind === "engine.login.stop" && isHostOpId(frame.opId),
576
+ );
577
+ }
578
+
579
+ export function isEngineLoginEventFrame(
580
+ value: unknown,
581
+ ): value is EngineLoginEventFrame {
582
+ const frame = exactWireObject(
583
+ value,
584
+ ["kind", "opId", "engine", "phase"],
585
+ ["url", "errorCode", "message"],
586
+ );
587
+ if (
588
+ !frame ||
589
+ frame.kind !== "engine.login.event" ||
590
+ !isHostOpId(frame.opId) ||
591
+ !isEngineLoginKind(frame.engine) ||
592
+ (Object.hasOwn(frame, "message") && !isBoundedMessage(frame.message))
593
+ ) {
594
+ return false;
595
+ }
596
+ if (frame.phase === "authorize") {
597
+ return (
598
+ isHttpsUrl(frame.url) &&
599
+ !Object.hasOwn(frame, "errorCode")
600
+ );
601
+ }
602
+ if (Object.hasOwn(frame, "url")) return false;
603
+ if (frame.phase === "failed" || frame.phase === "timed_out") {
604
+ return (
605
+ !Object.hasOwn(frame, "errorCode") ||
606
+ isEngineLoginErrorCode(frame.errorCode)
607
+ );
608
+ }
609
+ return (
610
+ (frame.phase === "starting" ||
611
+ frame.phase === "awaiting_input" ||
612
+ frame.phase === "awaiting_callback" ||
613
+ frame.phase === "succeeded" ||
614
+ frame.phase === "cancelled") &&
615
+ !Object.hasOwn(frame, "errorCode")
616
+ );
617
+ }
618
+
619
+ export function isEngineLoginCapability(
620
+ value: unknown,
621
+ ): value is EngineLoginCapability {
622
+ const capability = exactWireObject(value, [
623
+ "engine",
624
+ "status",
625
+ "loginSupported",
626
+ ]);
627
+ return Boolean(
628
+ capability &&
629
+ isEngineLoginKind(capability.engine) &&
630
+ (capability.status === "configured" || capability.status === "missing") &&
631
+ capability.loginSupported === true,
632
+ );
633
+ }
634
+
635
+ export function isHostConfigState(value: unknown): value is HostConfigState {
636
+ const config = exactWireObject(value, [
637
+ "agentCliAutoupdate",
638
+ "telemetryEnabled",
639
+ "retainTerminalHistory",
640
+ ]);
641
+ return Boolean(
642
+ config &&
643
+ typeof config.agentCliAutoupdate === "boolean" &&
644
+ typeof config.telemetryEnabled === "boolean" &&
645
+ typeof config.retainTerminalHistory === "boolean",
646
+ );
647
+ }
648
+
649
+ export function isHostConfigPatch(value: unknown): value is HostConfigPatch {
650
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
651
+ const patch = value as WireObject;
652
+ const keys = Object.keys(patch);
653
+ const allowed = new Set([
654
+ "agentCliAutoupdate",
655
+ "telemetryEnabled",
656
+ "retainTerminalHistory",
657
+ ]);
658
+ return (
659
+ keys.length >= 1 &&
660
+ keys.every(
661
+ (key) => allowed.has(key) && typeof patch[key] === "boolean",
662
+ )
663
+ );
664
+ }
665
+
666
+ export function isHostConfigGetFrame(
667
+ value: unknown,
668
+ ): value is HostConfigGetFrame {
669
+ const frame = exactWireObject(value, ["kind", "opId"]);
670
+ return Boolean(
671
+ frame && frame.kind === "host.config.get" && isHostOpId(frame.opId),
672
+ );
673
+ }
674
+
675
+ export function isHostConfigSetFrame(
676
+ value: unknown,
677
+ ): value is HostConfigSetFrame {
678
+ const frame = exactWireObject(value, ["kind", "opId", "patch"]);
679
+ return Boolean(
680
+ frame &&
681
+ frame.kind === "host.config.set" &&
682
+ isHostOpId(frame.opId) &&
683
+ isHostConfigPatch(frame.patch),
684
+ );
685
+ }
686
+
687
+ export function isHostConfigAckFrame(
688
+ value: unknown,
689
+ ): value is HostConfigAckFrame {
690
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
691
+ const candidate = value as WireObject;
692
+ if (candidate.ok === true) {
693
+ const frame = exactWireObject(value, [
694
+ "kind",
695
+ "opId",
696
+ "ok",
697
+ "config",
698
+ "restartRequired",
699
+ ]);
700
+ return Boolean(
701
+ frame &&
702
+ frame.kind === "host.config.ack" &&
703
+ isHostOpId(frame.opId) &&
704
+ isHostConfigState(frame.config) &&
705
+ typeof frame.restartRequired === "boolean",
706
+ );
707
+ }
708
+ if (candidate.ok === false) {
709
+ const frame = exactWireObject(value, [
710
+ "kind",
711
+ "opId",
712
+ "ok",
713
+ "errorCode",
714
+ "message",
715
+ ]);
716
+ return Boolean(
717
+ frame &&
718
+ frame.kind === "host.config.ack" &&
719
+ isHostOpId(frame.opId) &&
720
+ (frame.errorCode === "invalid_patch" ||
721
+ frame.errorCode === "persistence_failed") &&
722
+ isBoundedMessage(frame.message),
723
+ );
724
+ }
725
+ return false;
726
+ }
727
+
103
728
  /**
104
729
  * Host capability advertisement (ADR-021). Sent as a `host.capabilities`
105
730
  * frame after auth-success and re-sent when the host's adapter registry
@@ -131,6 +756,16 @@ export interface HostCapabilities {
131
756
  kind: string;
132
757
  availableVersions: string[];
133
758
  }>;
759
+ /** ADR-101: the machine-level container backend. Optional for rolling
760
+ * compatibility with hosts predating runtime detection. */
761
+ containerRuntime?: ContainerRuntimeCapability;
762
+ /** ADR-100: one-way boot latch. It becomes true after the initial shared
763
+ * image/CLI maintenance pass settles, including a best-effort failure. */
764
+ maintenanceReady?: boolean;
765
+ /** ADR-100: health of the independently-retrying host MCP gateway. */
766
+ mcpGateway?: McpGatewayCapability;
767
+ /** ADR-100: feature-gated, secret-free remote login availability. */
768
+ engineLogins?: EngineLoginCapability[];
134
769
  // ADR-033: cloud user ids that currently have a GitHub token ON THIS HOST —
135
770
  // the per-host gh-connected state shown on the host detail page. Re-advertised
136
771
  // whenever a token is added/removed. Optional: older hosts omit it.
@@ -190,6 +825,9 @@ export interface TaskUpResult {
190
825
  export interface TaskDownResult {
191
826
  status?: string;
192
827
  alreadyGone?: boolean;
828
+ /** Present only after an ADR-100 orphan-GC command has proved runtime,
829
+ * filesystem, credential/ACL, and host-SQLite task state absent. */
830
+ localStatePruned?: true;
193
831
  [key: string]: unknown;
194
832
  }
195
833
 
@@ -278,6 +916,28 @@ export interface TaskDownInput {
278
916
  taskId: string;
279
917
  task: TaskCommandTask;
280
918
  projects: TaskCommandProject[];
919
+ /** Optional and additive for rolling compatibility. Ordinary taskDown keeps
920
+ * terminal host history; orphan GC deletes task-owned local metadata only
921
+ * after the destructive teardown has been positively verified. */
922
+ cleanup?: {
923
+ kind: "orphan-gc";
924
+ pruneLocalState: true;
925
+ };
926
+ }
927
+
928
+ export type HostTaskInventorySource =
929
+ | "db"
930
+ | "session"
931
+ | "event"
932
+ | "workspace"
933
+ | "compose"
934
+ | "network"
935
+ | "volume"
936
+ | "preview";
937
+
938
+ export interface HostTaskInventoryEntry {
939
+ taskId: string;
940
+ sources: HostTaskInventorySource[];
281
941
  }
282
942
 
283
943
  export interface TaskDiffInput {
@@ -609,6 +1269,24 @@ export type CloudToHost =
609
1269
  // probe (headerValue, clientSecret) are write-only; nothing secret ever
610
1270
  // rides an ack. `opId` correlates request↔ack.
611
1271
  | { kind: "mcp.op"; opId: string; op: McpOp }
1272
+ // ADR-100 remote host operations. Each family is capability-gated by the
1273
+ // cloud during rolling deploys and parsed with the exact guards above.
1274
+ | HostLogsStartFrame
1275
+ | HostLogsStopFrame
1276
+ | EngineLoginStartFrame
1277
+ | EngineLoginInputFrame
1278
+ | EngineLoginCallbackFrame
1279
+ | EngineLoginStopFrame
1280
+ | HostConfigGetFrame
1281
+ | HostConfigSetFrame
1282
+ | {
1283
+ kind: "host.inventory.request";
1284
+ scanId: string;
1285
+ /** Null starts a new immutable snapshot. Otherwise this is the previous
1286
+ * page's opaque/safe task-id cursor. */
1287
+ cursor: string | null;
1288
+ limit: number;
1289
+ }
612
1290
  // ADR-103: sent once right after auth to a host that supplied a bootId.
613
1291
  // `afterSeq` is the cloud's replay watermark for this boot: the host
614
1292
  // replays every outbox entry with seq > afterSeq. `null` means the cloud
@@ -650,6 +1328,16 @@ export type HostToCloud =
650
1328
  // for pre-103 hosts (which also never send seq'd events).
651
1329
  | { kind: "auth"; token: string; hostId: string; bootId?: string }
652
1330
  | { kind: "host.capabilities"; capabilities: HostCapabilities }
1331
+ | {
1332
+ kind: "host.inventory.page";
1333
+ scanId: string;
1334
+ cursor: string | null;
1335
+ entries: HostTaskInventoryEntry[];
1336
+ nextCursor: string | null;
1337
+ /** False means one or more Docker label queries failed. Positive
1338
+ * evidence from successful sources remains valid and is still sent. */
1339
+ runtimeEnumerated: boolean;
1340
+ }
653
1341
  | {
654
1342
  kind: "result";
655
1343
  commandId: string;
@@ -733,7 +1421,13 @@ export type HostToCloud =
733
1421
  authorizeUrl?: string;
734
1422
  scopes?: string[];
735
1423
  }
736
- | { kind: "mcp.ack"; opId: string; ok: false; error: string };
1424
+ | { kind: "mcp.ack"; opId: string; ok: false; error: string }
1425
+ // ADR-100 remote operation output remains outside the durable task-event
1426
+ // outbox: it belongs to one authenticated bridge connection and opId.
1427
+ | HostLogsChunkFrame
1428
+ | HostLogsEndFrame
1429
+ | EngineLoginEventFrame
1430
+ | HostConfigAckFrame;
737
1431
 
738
1432
  export type HostEvent =
739
1433
  | {