@runuai/host 0.9.119 → 0.9.121

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/src/main.ts CHANGED
@@ -248,6 +248,10 @@ import {
248
248
  isEngineAccountLabel,
249
249
  isEngineConnectKind,
250
250
  } from "./protocol";
251
+ import { isEnvironmentTransportProvider } from "../lib/task-environment/types";
252
+ import { LeaseManager } from "../lib/lease";
253
+ import { sidecarMode, sidecarTaskId } from "../lib/sidecar";
254
+ import { declareAgentClisReady } from "../lib/standard-image";
251
255
 
252
256
  setContainerRuntimeOperationalCheck(() => containerRuntimeProblem() === null);
253
257
 
@@ -457,24 +461,53 @@ migrateHostDb();
457
461
  // credential store. Import what this host lacks and upload what was never
458
462
  // mirrored, off the startup path — task-up resolves locally either way.
459
463
  configureEngineAccountStore({ hostId });
460
- void reconcileEngineAccountsFromStore({ hostId })
461
- .then((r) => {
462
- if (r.skipped) return;
463
- console.log(
464
- `[engine-accounts] credential store reconciled: ${r.imported} imported, ${r.uploaded} uploaded, ${r.removed} removed, ${r.errors} errors`,
465
- );
466
- })
467
- .catch((error: unknown) => {
468
- console.warn(
469
- `[engine-accounts] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
470
- );
471
- });
464
+ /** The credential-store reconciles. At boot on a host with its own AWS
465
+ * identity; after the first lease on a sidecar (ADR-133), which has no
466
+ * credentials before that. */
467
+ function reconcileStores(): void {
468
+ void reconcileEngineAccountsFromStore({ hostId })
469
+ .then((r) => {
470
+ if (r.skipped) return;
471
+ console.log(
472
+ `[engine-accounts] credential store reconciled: ${r.imported} imported, ${r.uploaded} uploaded, ${r.removed} removed, ${r.errors} errors`,
473
+ );
474
+ })
475
+ .catch((error: unknown) => {
476
+ console.warn(
477
+ `[engine-accounts] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
478
+ );
479
+ });
480
+ void reconcileUserCredentialsFromStore()
481
+ .then((r) => {
482
+ if (r.skipped) return;
483
+ console.log(
484
+ `[user-credentials] credential store reconciled: ${r.imported} imported, ${r.uploaded} uploaded, ${r.removed} removed, ${r.errors} errors`,
485
+ );
486
+ })
487
+ .catch((error: unknown) => {
488
+ console.warn(
489
+ `[user-credentials] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
490
+ );
491
+ });
492
+ void reconcileProjectEnvFromStore()
493
+ .then((r) => {
494
+ if (r.skipped) return;
495
+ console.log(
496
+ `[project-env] credential store reconciled: ${r.imported} imported, ${r.uploaded} uploaded, ${r.removed} removed, ${r.errors} errors`,
497
+ );
498
+ })
499
+ .catch((error: unknown) => {
500
+ console.warn(
501
+ `[project-env] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
502
+ );
503
+ });
504
+ }
472
505
  // Image + agent-CLI maintenance runs where the compute is owned: an owned
473
506
  // host keeps its own image current on an hourly schedule (a task-up reuses
474
507
  // the last pass instead of blocking on one); a Uai-operated host sets
475
508
  // UAI_IMAGE_MAINTENANCE=external — a cloud job keeps the payload AMI fresh —
476
- // and reconciles only once, at start.
477
- if (env.UAI_IMAGE_MAINTENANCE === "host") {
509
+ // and reconciles only once, at start. A sidecar (ADR-133) has no image.
510
+ if (env.UAI_IMAGE_MAINTENANCE === "host" && !sidecarMode()) {
478
511
  const maintenanceTimer = setInterval(() => {
479
512
  void ensureStandardImage().catch(() => undefined);
480
513
  }, MAINTENANCE_INTERVAL_MS);
@@ -489,38 +522,50 @@ const repushBrokeredCopies = async (kind: string): Promise<void> => {
489
522
  await getOrchestrator().refreshEngineAccountCopies(kind);
490
523
  };
491
524
  configureTokenBroker({ repush: repushBrokeredCopies });
492
- startTokenBrokerLoop();
493
- startCredentialStorePickup({ repush: repushBrokeredCopies });
494
- void reconcileUserCredentialsFromStore()
495
- .then((r) => {
496
- if (r.skipped) return;
497
- console.log(
498
- `[user-credentials] credential store reconciled: ${r.imported} imported, ${r.uploaded} uploaded, ${r.removed} removed, ${r.errors} errors`,
499
- );
500
- })
501
- .catch((error: unknown) => {
502
- console.warn(
503
- `[user-credentials] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
504
- );
505
- });
506
- void reconcileProjectEnvFromStore()
507
- .then((r) => {
508
- if (r.skipped) return;
509
- console.log(
510
- `[project-env] credential store reconciled: ${r.imported} imported, ${r.uploaded} uploaded, ${r.removed} removed, ${r.errors} errors`,
511
- );
512
- })
513
- .catch((error: unknown) => {
514
- console.warn(
515
- `[project-env] credential store reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
516
- );
517
- });
525
+ // ADR-133: a sidecar never refreshes sign-ins (the broker Lambda does), and
526
+ // reads the store only once it holds a lease — see leaseManager below.
527
+ if (!sidecarMode()) {
528
+ startTokenBrokerLoop();
529
+ startCredentialStorePickup({ repush: repushBrokeredCopies });
530
+ reconcileStores();
531
+ }
532
+ /**
533
+ * ADR-133 sidecar: the task-scoped AWS lease. Requested when the bridge is
534
+ * ready, refreshed before expiry; the first grant is what lets the store
535
+ * reconciles and the S3 file mounts run. A denial means the task is no
536
+ * longer ours to run — exit 3 and let the cloud's sweep reclaim the machine.
537
+ */
538
+ const leaseManager: LeaseManager | null = sidecarMode()
539
+ ? new LeaseManager({
540
+ taskId: sidecarTaskId()!,
541
+ send: (frame) => {
542
+ if (!ws || ws.readyState !== WebSocket.OPEN) return false;
543
+ send(ws, frame);
544
+ return true;
545
+ },
546
+ onFirstGrant: () => {
547
+ console.log("[lease] granted — reconciling the credential store");
548
+ reconcileStores();
549
+ startCredentialStorePickup({ repush: repushBrokeredCopies });
550
+ },
551
+ onGrant: (lease) => {
552
+ console.log(
553
+ `[lease] holds until ${new Date(lease.expiresAt).toISOString()}`,
554
+ );
555
+ },
556
+ onDenied: (error) => {
557
+ console.error(`[lease] denied: ${error} — this machine no longer runs its task`);
558
+ process.exit(3);
559
+ },
560
+ })
561
+ : null;
562
+ if (sidecarMode()) declareAgentClisReady();
518
563
  // Reconcile a durable retention-off setting before recovery/connect can make
519
564
  // crash-left owner-facing terminal history observable again. Correctness
520
565
  // tombstones remain intact inside applyTerminalHistoryRetention.
521
566
  applyTerminalHistoryRetention();
522
567
  try {
523
- await configureBundledContainerRuntime();
568
+ if (!sidecarMode()) await configureBundledContainerRuntime();
524
569
  } catch {
525
570
  // A malformed/tampered active generation must never fall back to PATH or an
526
571
  // inferred asset layout. Keep Docker-first operation available and surface
@@ -529,7 +574,17 @@ try {
529
574
  "[host-agent] signed bundled runtime validation failed; the bundled runtime is disabled",
530
575
  );
531
576
  }
532
- const initialRuntime = await initializeContainerRuntime();
577
+ const initialRuntime: Awaited<ReturnType<typeof initializeContainerRuntime>> =
578
+ sidecarMode()
579
+ ? // ADR-133: a task machine runs its agents directly; nothing here ever
580
+ // dials a container runtime.
581
+ {
582
+ status: "no-runtime",
583
+ preference: "auto",
584
+ message: "sidecar mode — no container runtime on a task machine",
585
+ action: "none: a task machine runs its agents directly",
586
+ }
587
+ : await initializeContainerRuntime();
533
588
  // Do not touch the selected daemon merely because detection succeeded. The
534
589
  // activation gate below must first prove that every durable TaskEnvironment
535
590
  // belongs to this exact provider/endpoint. The manager's first admitted login
@@ -612,12 +667,16 @@ const stopAgentClisReadySubscription = onAgentClisReady(() =>
612
667
  void connect();
613
668
  // Local browser UI (ADR-028) — same single process, alongside the WSS client.
614
669
  // Best-effort: a UI bind failure must not take the host service down.
615
- void startLocalUi();
670
+ // A task machine has no operator sitting at it (ADR-133): no local UI, no
671
+ // Mac vnode watch.
672
+ if (!sidecarMode()) void startLocalUi();
616
673
  // ADR-118 follow-up: vnode-pressure observability (macOS/apple-container
617
674
  // hosts). Logs only on band transitions; the sensor caches its sysctl read.
618
- const vnodePressureTimer = setInterval(() => void logVnodePressure(), 60_000);
619
- vnodePressureTimer.unref?.();
620
- void logVnodePressure();
675
+ if (!sidecarMode()) {
676
+ const vnodePressureTimer = setInterval(() => void logVnodePressure(), 60_000);
677
+ vnodePressureTimer.unref?.();
678
+ void logVnodePressure();
679
+ }
621
680
 
622
681
  async function startLocalUi(): Promise<void> {
623
682
  try {
@@ -717,6 +776,7 @@ function buildCapabilities(): HostCapabilities {
717
776
  },
718
777
  }
719
778
  : {}),
779
+ ...(sidecarTaskId() ? { sidecar: { taskId: sidecarTaskId()! } } : {}),
720
780
  maintenanceReady: areAgentClisReady(),
721
781
  mcpGateway: mcpGateway.state(),
722
782
  engineLogins: engineLoginManager.capabilities(),
@@ -1206,6 +1266,7 @@ async function connect(): Promise<void> {
1206
1266
  reconnectAttempt = 0;
1207
1267
  markConnected();
1208
1268
  console.log(`[host-agent] connected as ${hostId}`);
1269
+ leaseManager?.reconnected();
1209
1270
  setHostObsTag(hostId);
1210
1271
  addHostBreadcrumb("bridge", "connected");
1211
1272
 
@@ -1405,6 +1466,10 @@ async function connect(): Promise<void> {
1405
1466
  case "tunnel.close":
1406
1467
  closeTunnel(socket, frame.tunnelId, frame.reason);
1407
1468
  break;
1469
+ case "lease.grant":
1470
+ case "lease.deny":
1471
+ leaseManager?.handle(frame);
1472
+ break;
1408
1473
  case "gh.connect.set": {
1409
1474
  const runtimeProblem = containerRuntimeProblem();
1410
1475
  if (runtimeProblem) {
@@ -2141,7 +2206,7 @@ async function resolveTunnelTarget(
2141
2206
  // operational. After a daemon restart an unrelated local process could bind
2142
2207
  // a stale port before recovery refreshes the task's mappings.
2143
2208
  const machineTask = getHostTask(frame.taskId);
2144
- if (machineTask?.environmentProvider === "machine") {
2209
+ if (machineTask && isEnvironmentTransportProvider(machineTask.environmentProvider)) {
2145
2210
  // ADR-121: a machine's address is orchestrator-routable (bridge IP on
2146
2211
  // the Linux guinea pig, VPC-private on EC2) — editor and previews dial
2147
2212
  // it straight via the handle's ownership-proven route, independent of
@@ -3100,6 +3165,9 @@ function rejectFrameDuringShutdown(
3100
3165
  case "tunnel.data":
3101
3166
  case "tunnel.requestEnd":
3102
3167
  case "tunnel.close":
3168
+ // A lease answer is housekeeping the sidecar still wants during a drain.
3169
+ case "lease.grant":
3170
+ case "lease.deny":
3103
3171
  return false;
3104
3172
  }
3105
3173
  }
package/src/protocol.ts CHANGED
@@ -1074,6 +1074,9 @@ export interface HostCapabilities {
1074
1074
  * Present when the host is configured for them; the cloud shows the
1075
1075
  * provider/region on the host page and, eventually, per-task backing
1076
1076
  * choice keys off it. */
1077
+ /** ADR-133 sidecar mode: this host IS the task's machine — exactly one
1078
+ * task, no container runtime, tunnels on loopback. */
1079
+ sidecar?: { taskId: string };
1077
1080
  machineTasks?: {
1078
1081
  provider: string;
1079
1082
  region?: string;
@@ -1695,9 +1698,32 @@ export type CloudToHost =
1695
1698
  // only entries it never transmitted, replaying nothing.
1696
1699
  | { kind: "event.resume"; afterSeq: number | null }
1697
1700
  // ADR-103: cumulative — the host trims outbox entries with seq <= seq.
1698
- | { kind: "event.ack"; seq: number };
1701
+ | { kind: "event.ack"; seq: number }
1702
+ // ADR-133 leases: the cloud's answer to a sidecar's `lease.request`.
1703
+ | { kind: "lease.grant"; opId: string; lease: TaskLease }
1704
+ | { kind: "lease.deny"; opId: string; error: string };
1699
1705
 
1700
1706
  /** One MCP-connection operation (ADR-057), executed by the host. */
1707
+ /**
1708
+ * ADR-133: the short-lived AWS credentials a task machine runs on — an STS
1709
+ * session tagged with the task's workspace and owner, issued by the cloud
1710
+ * over the bridge and held in the sidecar's memory only. `expiresAt` is the
1711
+ * session expiry (epoch ms); the sidecar re-requests before it.
1712
+ */
1713
+ export interface TaskLease {
1714
+ accessKeyId: string;
1715
+ secretAccessKey: string;
1716
+ sessionToken: string;
1717
+ expiresAt: number;
1718
+ region: string;
1719
+ /** Files bucket and this task's prefixes inside it (S3 shared files). */
1720
+ files: {
1721
+ bucket: string;
1722
+ orgPrefix: string | null;
1723
+ userPrefix: string;
1724
+ };
1725
+ }
1726
+
1701
1727
  export type McpOp =
1702
1728
  // Probe `url` (MCP initialize). No auth → connected. 401 → walk the OAuth
1703
1729
  // discovery chain (RFC 9728 → 8414 → 7591), prepare PKCE, and ack
@@ -1751,6 +1777,8 @@ export type HostToCloud =
1751
1777
  // a task's containers, so the cloud mirrors the status (currently "stopped").
1752
1778
  | { kind: "task.status"; taskId: string; status: string }
1753
1779
  | { kind: "ping"; ts: number }
1780
+ // ADR-133: a sidecar asks for (or refreshes) its task-scoped AWS lease.
1781
+ | { kind: "lease.request"; opId: string; taskId: string }
1754
1782
  | {
1755
1783
  kind: "tunnel.ack";
1756
1784
  tunnelId: string;