@runuai/host 0.9.120 → 0.9.122

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 { markSidecarReady, sidecarMode, sidecarTaskId } from "../lib/sidecar";
254
+ import { declareAgentClisReady } from "../lib/standard-image";
251
255
 
252
256
  setContainerRuntimeOperationalCheck(() => containerRuntimeProblem() === null);
253
257
 
@@ -457,24 +461,54 @@ 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(): Promise<void> {
468
+ const engineAccounts = 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
+ const userCredentials = 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
+ const projectEnv = 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
+ return Promise.all([engineAccounts, userCredentials, projectEnv]).then(() => undefined);
505
+ }
472
506
  // Image + agent-CLI maintenance runs where the compute is owned: an owned
473
507
  // host keeps its own image current on an hourly schedule (a task-up reuses
474
508
  // the last pass instead of blocking on one); a Uai-operated host sets
475
509
  // 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") {
510
+ // and reconciles only once, at start. A sidecar (ADR-133) has no image.
511
+ if (env.UAI_IMAGE_MAINTENANCE === "host" && !sidecarMode()) {
478
512
  const maintenanceTimer = setInterval(() => {
479
513
  void ensureStandardImage().catch(() => undefined);
480
514
  }, MAINTENANCE_INTERVAL_MS);
@@ -489,38 +523,55 @@ const repushBrokeredCopies = async (kind: string): Promise<void> => {
489
523
  await getOrchestrator().refreshEngineAccountCopies(kind);
490
524
  };
491
525
  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
- });
526
+ // ADR-133: a sidecar never refreshes sign-ins (the broker Lambda does), and
527
+ // reads the store only once it holds a lease — see leaseManager below.
528
+ if (!sidecarMode()) {
529
+ startTokenBrokerLoop();
530
+ startCredentialStorePickup({ repush: repushBrokeredCopies });
531
+ void reconcileStores();
532
+ }
533
+ /**
534
+ * ADR-133 sidecar: the task-scoped AWS lease. Requested when the bridge is
535
+ * ready, refreshed before expiry; the first grant is what lets the store
536
+ * reconciles and the S3 file mounts run. A denial means the task is no
537
+ * longer ours to run — exit 3 and let the cloud's sweep reclaim the machine.
538
+ */
539
+ const leaseManager: LeaseManager | null = sidecarMode()
540
+ ? new LeaseManager({
541
+ taskId: sidecarTaskId()!,
542
+ send: (frame) => {
543
+ if (!ws || ws.readyState !== WebSocket.OPEN) return false;
544
+ send(ws, frame);
545
+ return true;
546
+ },
547
+ onFirstGrant: () => {
548
+ console.log("[lease] granted — reconciling the credential store");
549
+ void reconcileStores().finally(() => {
550
+ // Task-up waits for this: only now are engine accounts, GitHub
551
+ // tokens, and project env on the machine (lib/sidecar.ts).
552
+ markSidecarReady();
553
+ console.log("[lease] credential store reconciled — the task may start");
554
+ });
555
+ startCredentialStorePickup({ repush: repushBrokeredCopies });
556
+ },
557
+ onGrant: (lease) => {
558
+ console.log(
559
+ `[lease] holds until ${new Date(lease.expiresAt).toISOString()}`,
560
+ );
561
+ },
562
+ onDenied: (error) => {
563
+ console.error(`[lease] denied: ${error} — this machine no longer runs its task`);
564
+ process.exit(3);
565
+ },
566
+ })
567
+ : null;
568
+ if (sidecarMode()) declareAgentClisReady();
518
569
  // Reconcile a durable retention-off setting before recovery/connect can make
519
570
  // crash-left owner-facing terminal history observable again. Correctness
520
571
  // tombstones remain intact inside applyTerminalHistoryRetention.
521
572
  applyTerminalHistoryRetention();
522
573
  try {
523
- await configureBundledContainerRuntime();
574
+ if (!sidecarMode()) await configureBundledContainerRuntime();
524
575
  } catch {
525
576
  // A malformed/tampered active generation must never fall back to PATH or an
526
577
  // inferred asset layout. Keep Docker-first operation available and surface
@@ -529,7 +580,17 @@ try {
529
580
  "[host-agent] signed bundled runtime validation failed; the bundled runtime is disabled",
530
581
  );
531
582
  }
532
- const initialRuntime = await initializeContainerRuntime();
583
+ const initialRuntime: Awaited<ReturnType<typeof initializeContainerRuntime>> =
584
+ sidecarMode()
585
+ ? // ADR-133: a task machine runs its agents directly; nothing here ever
586
+ // dials a container runtime.
587
+ {
588
+ status: "no-runtime",
589
+ preference: "auto",
590
+ message: "sidecar mode — no container runtime on a task machine",
591
+ action: "none: a task machine runs its agents directly",
592
+ }
593
+ : await initializeContainerRuntime();
533
594
  // Do not touch the selected daemon merely because detection succeeded. The
534
595
  // activation gate below must first prove that every durable TaskEnvironment
535
596
  // belongs to this exact provider/endpoint. The manager's first admitted login
@@ -612,12 +673,16 @@ const stopAgentClisReadySubscription = onAgentClisReady(() =>
612
673
  void connect();
613
674
  // Local browser UI (ADR-028) — same single process, alongside the WSS client.
614
675
  // Best-effort: a UI bind failure must not take the host service down.
615
- void startLocalUi();
676
+ // A task machine has no operator sitting at it (ADR-133): no local UI, no
677
+ // Mac vnode watch.
678
+ if (!sidecarMode()) void startLocalUi();
616
679
  // ADR-118 follow-up: vnode-pressure observability (macOS/apple-container
617
680
  // 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();
681
+ if (!sidecarMode()) {
682
+ const vnodePressureTimer = setInterval(() => void logVnodePressure(), 60_000);
683
+ vnodePressureTimer.unref?.();
684
+ void logVnodePressure();
685
+ }
621
686
 
622
687
  async function startLocalUi(): Promise<void> {
623
688
  try {
@@ -717,6 +782,7 @@ function buildCapabilities(): HostCapabilities {
717
782
  },
718
783
  }
719
784
  : {}),
785
+ ...(sidecarTaskId() ? { sidecar: { taskId: sidecarTaskId()! } } : {}),
720
786
  maintenanceReady: areAgentClisReady(),
721
787
  mcpGateway: mcpGateway.state(),
722
788
  engineLogins: engineLoginManager.capabilities(),
@@ -1206,6 +1272,7 @@ async function connect(): Promise<void> {
1206
1272
  reconnectAttempt = 0;
1207
1273
  markConnected();
1208
1274
  console.log(`[host-agent] connected as ${hostId}`);
1275
+ leaseManager?.reconnected();
1209
1276
  setHostObsTag(hostId);
1210
1277
  addHostBreadcrumb("bridge", "connected");
1211
1278
 
@@ -1405,6 +1472,10 @@ async function connect(): Promise<void> {
1405
1472
  case "tunnel.close":
1406
1473
  closeTunnel(socket, frame.tunnelId, frame.reason);
1407
1474
  break;
1475
+ case "lease.grant":
1476
+ case "lease.deny":
1477
+ leaseManager?.handle(frame);
1478
+ break;
1408
1479
  case "gh.connect.set": {
1409
1480
  const runtimeProblem = containerRuntimeProblem();
1410
1481
  if (runtimeProblem) {
@@ -2141,7 +2212,7 @@ async function resolveTunnelTarget(
2141
2212
  // operational. After a daemon restart an unrelated local process could bind
2142
2213
  // a stale port before recovery refreshes the task's mappings.
2143
2214
  const machineTask = getHostTask(frame.taskId);
2144
- if (machineTask?.environmentProvider === "machine") {
2215
+ if (machineTask && isEnvironmentTransportProvider(machineTask.environmentProvider)) {
2145
2216
  // ADR-121: a machine's address is orchestrator-routable (bridge IP on
2146
2217
  // the Linux guinea pig, VPC-private on EC2) — editor and previews dial
2147
2218
  // it straight via the handle's ownership-proven route, independent of
@@ -3100,6 +3171,9 @@ function rejectFrameDuringShutdown(
3100
3171
  case "tunnel.data":
3101
3172
  case "tunnel.requestEnd":
3102
3173
  case "tunnel.close":
3174
+ // A lease answer is housekeeping the sidecar still wants during a drain.
3175
+ case "lease.grant":
3176
+ case "lease.deny":
3103
3177
  return false;
3104
3178
  }
3105
3179
  }
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;