@runuai/host 0.9.79 → 0.9.81

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.
@@ -50,6 +50,7 @@ import {
50
50
  } from "./task-container-cli";
51
51
  import { MCP_CONFIG_LOCK_PATH } from "./mcp-config-lock";
52
52
  import {
53
+ MACHINE_RUNTIME_AUTHORITY_ENV,
53
54
  RUNTIME_AUTHORITY_ENV,
54
55
  runtimeAuthorityDockerExecArgs,
55
56
  } from "./runtime-authority";
@@ -994,8 +995,19 @@ export async function setupBrowserTesting(
994
995
  containerName: string,
995
996
  hasCodex: boolean,
996
997
  codexHomes: readonly string[] = hasCodex ? [DEFAULT_CODEX_HOME] : [],
997
- environment?: Pick<TaskEnvironmentHandle, "exec" | "launchDetachedSession">,
998
+ environment?: Pick<TaskEnvironmentHandle, "exec" | "launchDetachedSession"> &
999
+ Partial<Pick<TaskEnvironmentHandle, "descriptor">>,
998
1000
  ): Promise<BrowserSetup> {
1001
+ // Machine sessions run under MACHINE_RUNTIME_AUTHORITY_ENV, so every setup
1002
+ // exec here must too — the prewarm in particular. Warming npx under the
1003
+ // CONTAINER authority (private NPM_CONFIG_CACHE) leaves the session-default
1004
+ // npm cache cold, and Codex's 10s MCP startup deadline then kills the
1005
+ // browser (and every npx-launched server) for the session's whole lifetime
1006
+ // while Claude's 30s survives — live 2026-08-28, first codex machine task.
1007
+ const authorityEnv =
1008
+ environment?.descriptor?.locator.provider === "machine"
1009
+ ? MACHINE_RUNTIME_AUTHORITY_ENV
1010
+ : RUNTIME_AUTHORITY_ENV;
999
1011
  let ready = false;
1000
1012
  let configured = false;
1001
1013
  let changed = false;
@@ -1029,7 +1041,7 @@ export async function setupBrowserTesting(
1029
1041
  if (environment) {
1030
1042
  await environment.launchDetachedSession({
1031
1043
  argv: ["sh", "-c", step],
1032
- env: RUNTIME_AUTHORITY_ENV,
1044
+ env: authorityEnv,
1033
1045
  maxOutputBytes: 64 * 1024,
1034
1046
  launchTimeoutMs: 10_000,
1035
1047
  });
@@ -1067,14 +1079,14 @@ export async function setupBrowserTesting(
1067
1079
  await environment.exec({
1068
1080
  argv: ["/usr/bin/chown", "node:node", "/opt/pw-browsers"],
1069
1081
  user: "root",
1070
- env: RUNTIME_AUTHORITY_ENV,
1082
+ env: authorityEnv,
1071
1083
  timeoutMs: 5_000,
1072
1084
  maxOutputBytes: 64 * 1024,
1073
1085
  });
1074
1086
  return environment.exec({
1075
1087
  argv: ["sh", "-lc", PREWARM_CMD],
1076
1088
  cwd: "/workspace",
1077
- env: RUNTIME_AUTHORITY_ENV,
1089
+ env: authorityEnv,
1078
1090
  timeoutMs: INSTALL_TIMEOUT_MS,
1079
1091
  maxOutputBytes: 256 * 1024,
1080
1092
  });
@@ -1140,7 +1152,7 @@ export async function setupBrowserTesting(
1140
1152
  (10 + Math.max(45, 15 + selectedCodexHomes.length * 12) + 5 + 10) *
1141
1153
  1_000;
1142
1154
  const writeEnv = {
1143
- ...RUNTIME_AUTHORITY_ENV,
1155
+ ...authorityEnv,
1144
1156
  UAI_BROWSER_DEF: JSON.stringify(SERVER_DEF),
1145
1157
  UAI_MCP_PATH: MCP_CONFIG_PATH,
1146
1158
  UAI_CLAUDE_SETTINGS_PATH: CLAUDE_SETTINGS_PATH,
@@ -1276,7 +1288,7 @@ export async function setupBrowserTesting(
1276
1288
  ].join("; ");
1277
1289
 
1278
1290
  const rootWorkerEnv = {
1279
- ...RUNTIME_AUTHORITY_ENV,
1291
+ ...authorityEnv,
1280
1292
  NPM_CONFIG_CACHE: "/tmp/uai-root-npm-cache",
1281
1293
  npm_config_cache: "/tmp/uai-root-npm-cache",
1282
1294
  NPM_CONFIG_LOGS_DIR: "/tmp/uai-root-npm-cache/_logs",
@@ -42,8 +42,14 @@ export interface AwsMachineConfig {
42
42
  /** Override the size ladder; the default picks the smallest Graviton
43
43
  * type satisfying both cpu and memory. */
44
44
  instanceType?: (spec: MachineSpec) => string;
45
+ /** Root EBS volume size. The AMI snapshot default (8 GiB Debian) starves
46
+ * real work — a monorepo install plus a browser left ~36 MB free live
47
+ * (2026-08-28). cloud-init's growpart expands the filesystem on boot. */
48
+ diskGiB?: number;
45
49
  }
46
50
 
51
+ const DEFAULT_DISK_GIB = 50;
52
+
47
53
  type Runner = (args: string[]) => Promise<DockerResult>;
48
54
 
49
55
  const DEFAULT_TIMEOUT_MS = 60_000;
@@ -224,6 +230,41 @@ export function createAwsMachineProvider(
224
230
  return machineId.slice("uai-machine-".length);
225
231
  };
226
232
 
233
+ /** Root device name per AMI, resolved once per process. A failure throws:
234
+ * guessing the device would silently attach a blank second volume while
235
+ * the root stays at its 8 GiB snapshot size. */
236
+ const rootDeviceByImage = new Map<string, string>();
237
+ async function rootDeviceName(image: string): Promise<string> {
238
+ const cached = rootDeviceByImage.get(image);
239
+ if (cached) return cached;
240
+ const res = await runner([
241
+ "ec2",
242
+ "describe-images",
243
+ "--image-ids",
244
+ image,
245
+ "--query",
246
+ "Images[0].RootDeviceName",
247
+ ]);
248
+ if (res.status !== 0) {
249
+ throw new Error(
250
+ `could not resolve the root device of ${image}: ${
251
+ res.stderr.trim() || `exit ${res.status ?? "killed"}`
252
+ }`,
253
+ );
254
+ }
255
+ let device: unknown;
256
+ try {
257
+ device = JSON.parse(res.stdout);
258
+ } catch {
259
+ device = null;
260
+ }
261
+ if (typeof device !== "string" || !device.startsWith("/dev/")) {
262
+ throw new Error(`image ${image} reports no root device name`);
263
+ }
264
+ rootDeviceByImage.set(image, device);
265
+ return device;
266
+ }
267
+
227
268
  /** Resolve the logical machine id to its live instances via the tag. */
228
269
  async function resolveLive(
229
270
  machineId: string,
@@ -292,6 +333,12 @@ export function createAwsMachineProvider(
292
333
  // identical retry still cannot mint a second instance; a CHANGED
293
334
  // launch legitimately gets a fresh token, and the resume-aware
294
335
  // provision's describe-first step remains the guard against doubling
336
+ // The AMI snapshot's root volume (8 GiB on the Debian payload) starves
337
+ // real work; request a task-sized gp3 root instead. The mapping must
338
+ // name the AMI's actual root device — a wrong name would ADD a blank
339
+ // volume while the root stays small — so resolve it per image.
340
+ const diskGiB = config.diskGiB ?? DEFAULT_DISK_GIB;
341
+ const rootDevice = await rootDeviceName(spec.image);
295
342
  // a live machine.
296
343
  const launchFingerprint = createHash("sha256")
297
344
  .update(
@@ -302,6 +349,7 @@ export function createAwsMachineProvider(
302
349
  config.subnetId ?? null,
303
350
  config.securityGroupId ?? null,
304
351
  config.iamInstanceProfileArn ?? null,
352
+ diskGiB,
305
353
  ]),
306
354
  )
307
355
  .digest("hex")
@@ -321,6 +369,17 @@ export function createAwsMachineProvider(
321
369
  `${machineId}-${launchFingerprint}`.slice(0, 64),
322
370
  "--tag-specifications",
323
371
  `ResourceType=instance,Tags=[{Key=${AWS_MACHINE_TAG},Value=${spec.taskId}},{Key=Name,Value=${machineId}}]`,
372
+ "--block-device-mappings",
373
+ JSON.stringify([
374
+ {
375
+ DeviceName: rootDevice,
376
+ Ebs: {
377
+ VolumeSize: diskGiB,
378
+ VolumeType: "gp3",
379
+ DeleteOnTermination: true,
380
+ },
381
+ },
382
+ ]),
324
383
  ...(spec.authorizedPublicKey
325
384
  ? ["--user-data", awsUserData(spec.authorizedPublicKey)]
326
385
  : []),
@@ -340,7 +399,20 @@ export function createAwsMachineProvider(
340
399
  }`,
341
400
  );
342
401
  }
343
- const info = await describe(machineId);
402
+ // DescribeInstances TAG FILTERS are eventually consistent: the query
403
+ // can miss an instance RunInstances just created (live 2026-08-28 —
404
+ // the machine was running the whole time while launch reported
405
+ // "absent"). Absence immediately after a successful launch is
406
+ // therefore not evidence yet; give the filter a bounded window.
407
+ let info = await describe(machineId);
408
+ for (
409
+ let attempt = 0;
410
+ (info.state === "absent" || info.state === "unknown") && attempt < 5;
411
+ attempt += 1
412
+ ) {
413
+ await new Promise<void>((resolve) => setTimeout(resolve, 3_000));
414
+ info = await describe(machineId);
415
+ }
344
416
  if (info.state === "absent" || info.state === "unknown") {
345
417
  throw new Error(
346
418
  `machine ${machineId} launched but could not be described (${info.state}${
@@ -138,11 +138,13 @@ function machineBackend() {
138
138
  if (!region) {
139
139
  throw new Error("UAI_MACHINE_PROVIDER=aws requires UAI_AWS_REGION");
140
140
  }
141
+ const diskGiB = Number(process.env.UAI_MACHINE_DISK_GIB ?? "");
141
142
  return createAwsMachineProvider({
142
143
  region,
143
144
  subnetId: process.env.UAI_AWS_SUBNET_ID,
144
145
  securityGroupId: process.env.UAI_AWS_SECURITY_GROUP_ID,
145
146
  iamInstanceProfileArn: process.env.UAI_AWS_INSTANCE_PROFILE_ARN,
147
+ ...(Number.isInteger(diskGiB) && diskGiB > 0 ? { diskGiB } : {}),
146
148
  });
147
149
  }
148
150
  return createLocalMachineProvider();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.79",
3
+ "version": "0.9.81",
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>",