@runuai/host 0.9.73 → 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.
- package/lib/machine-provider-aws.ts +54 -5
- package/package.json +1 -1
- package/src/main.ts +13 -3
|
@@ -103,7 +103,12 @@ export function awsUserData(authorizedPublicKey: string): string {
|
|
|
103
103
|
const script = [
|
|
104
104
|
"#!/bin/sh",
|
|
105
105
|
"set -e",
|
|
106
|
-
|
|
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
|
-
|
|
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.
|
|
167
|
-
? record.
|
|
168
|
-
:
|
|
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
package/src/main.ts
CHANGED
|
@@ -252,6 +252,12 @@ const bridgeUrl =
|
|
|
252
252
|
let ws: WebSocket | null = null;
|
|
253
253
|
let stopping = false;
|
|
254
254
|
let fatal = false;
|
|
255
|
+
/** Auth-rejected (4001) retry cadence. A genuinely revoked credential costs
|
|
256
|
+
* one connection attempt per interval; a transient rejection (the bridge's
|
|
257
|
+
* own DB down during a cloud restart, an auth frame lost on a slow link)
|
|
258
|
+
* heals without an operator visit — the 2026-08-27 outage stranded every
|
|
259
|
+
* connected host overnight because 4001 was treated as terminal. */
|
|
260
|
+
const AUTH_REJECTED_RETRY_MS = 5 * 60_000;
|
|
255
261
|
let reconnectAttempt = 0;
|
|
256
262
|
let connectionRequestGeneration = 0;
|
|
257
263
|
let inFlight = 0;
|
|
@@ -1642,12 +1648,16 @@ async function connect(): Promise<void> {
|
|
|
1642
1648
|
return;
|
|
1643
1649
|
}
|
|
1644
1650
|
if (code === 4001) {
|
|
1645
|
-
fatal = true;
|
|
1646
1651
|
markDisconnected(text || "auth rejected by bridge");
|
|
1647
1652
|
console.error(
|
|
1648
|
-
`[host-agent] auth rejected by bridge${text ? `: ${text}` : ""}`,
|
|
1653
|
+
`[host-agent] auth rejected by bridge${text ? `: ${text}` : ""} — retrying in ${Math.round(AUTH_REJECTED_RETRY_MS / 60_000)} minutes`,
|
|
1649
1654
|
);
|
|
1650
|
-
|
|
1655
|
+
addHostBreadcrumb("bridge", "auth-rejected", {
|
|
1656
|
+
retryInMs: AUTH_REJECTED_RETRY_MS,
|
|
1657
|
+
});
|
|
1658
|
+
setTimeout(() => {
|
|
1659
|
+
void connect();
|
|
1660
|
+
}, AUTH_REJECTED_RETRY_MS);
|
|
1651
1661
|
return;
|
|
1652
1662
|
}
|
|
1653
1663
|
const delay = reconnectDelay();
|