@runuai/host 0.9.74 → 0.9.75

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.
@@ -103,7 +103,12 @@ export function awsUserData(authorizedPublicKey: string): string {
103
103
  const script = [
104
104
  "#!/bin/sh",
105
105
  "set -e",
106
- "id node >/dev/null 2>&1 || useradd -m -u 1000 -s /bin/bash node",
106
+ // uid 1000 is often TAKEN on stock cloud images (Debian's `admin`)
107
+ // insisting on it aborted the whole script under set -e and left no key
108
+ // installed for anyone (live 2026-08-27, first EC2 smoke). Prefer 1000,
109
+ // fall back to auto: on a machine the task owns the whole filesystem,
110
+ // so the exact uid is convention, not correctness.
111
+ "id node >/dev/null 2>&1 || useradd -m -u 1000 -s /bin/bash node 2>/dev/null || useradd -m -s /bin/bash node",
107
112
  "for account in node root; do",
108
113
  ' home=$(getent passwd "$account" | cut -d: -f6)',
109
114
  ' mkdir -p "$home/.ssh"',
@@ -114,7 +119,12 @@ export function awsUserData(authorizedPublicKey: string): string {
114
119
  "done",
115
120
  "",
116
121
  ].join("\n");
117
- return Buffer.from(script, "utf8").toString("base64");
122
+ // RAW script, not base64: AWS CLI v2 base64-encodes --user-data ITSELF.
123
+ // Pre-encoding double-encoded it, cloud-init saw garbage and silently ran
124
+ // nothing — no key installed for anyone (live 2026-08-27, second EC2
125
+ // smoke failure; the uid-1000 collision fixed just before was real too,
126
+ // but masked by this).
127
+ return script;
118
128
  }
119
129
 
120
130
  type LiveInstance = {
@@ -144,6 +154,7 @@ function parseReservations(stdout: string): LiveInstance[] | null {
144
154
  InstanceId?: unknown;
145
155
  State?: { Name?: unknown };
146
156
  PrivateIpAddress?: unknown;
157
+ PublicIpAddress?: unknown;
147
158
  Tags?: Array<{ Key?: unknown; Value?: unknown }>;
148
159
  };
149
160
  if (typeof record.InstanceId !== "string") return null;
@@ -162,10 +173,16 @@ function parseReservations(stdout: string): LiveInstance[] | null {
162
173
  instances.push({
163
174
  instanceId: record.InstanceId,
164
175
  state,
176
+ // Prefer the public address when the subnet assigns one: an
177
+ // orchestrator OUTSIDE the VPC (the local smoke, a dev box) can only
178
+ // dial that, while the production in-VPC orchestrator sees no public
179
+ // IP on its machines and keeps dialing private, unchanged.
165
180
  privateIp:
166
- typeof record.PrivateIpAddress === "string"
167
- ? record.PrivateIpAddress
168
- : null,
181
+ typeof record.PublicIpAddress === "string"
182
+ ? record.PublicIpAddress
183
+ : typeof record.PrivateIpAddress === "string"
184
+ ? record.PrivateIpAddress
185
+ : null,
169
186
  taskLabel,
170
187
  });
171
188
  }
@@ -325,6 +342,38 @@ export function createAwsMachineProvider(
325
342
  }`,
326
343
  );
327
344
  }
345
+ // EC2 sits in `stopping` for tens of seconds, and StartInstances
346
+ // refuses it with IncorrectInstanceState — an immediate stop→resume
347
+ // (a user pressing Resume right after Stop; the live 2026-08-27
348
+ // smoke) failed exactly there because our state map folds `stopping`
349
+ // into "stopped". Stop resolves only once the instance can actually
350
+ // be started again, mirroring terminate's poll-to-absent.
351
+ const deadline = Date.now() + 180_000;
352
+ for (;;) {
353
+ const raw = await runner([
354
+ "ec2",
355
+ "describe-instances",
356
+ "--instance-ids",
357
+ instance.instanceId,
358
+ "--query",
359
+ "Reservations[].Instances[].State.Name",
360
+ "--output",
361
+ "text",
362
+ ]);
363
+ const state = raw.status === 0 ? raw.stdout.trim() : "";
364
+ if (state === "stopped") return;
365
+ if (state === "terminated" || state === "shutting-down") {
366
+ throw new Error(
367
+ `machine ${machineId} terminated while stopping (${state})`,
368
+ );
369
+ }
370
+ if (Date.now() >= deadline) {
371
+ throw new Error(
372
+ `machine ${machineId} did not reach stopped within the stop deadline (${state || "unknown"})`,
373
+ );
374
+ }
375
+ await new Promise<void>((resolve) => setTimeout(resolve, 5_000));
376
+ }
328
377
  },
329
378
 
330
379
  async start(machineId: string): Promise<MachineInfo> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.74",
3
+ "version": "0.9.75",
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>",