@runuai/host 0.8.25 → 0.8.27

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.
@@ -305,13 +305,22 @@ async function upgradeVolumeAgentClis(): Promise<void> {
305
305
  }
306
306
  }
307
307
 
308
+ /** Outcome of ensureStandardImage — `error` carries WHY when not ready, so
309
+ * the taskUp path can surface a build failure to the cloud (not just the host
310
+ * log). Callers that only fire-and-forget at boot ignore the return. */
311
+ export interface StandardImageResult {
312
+ ok: boolean;
313
+ error?: string;
314
+ }
315
+
308
316
  /**
309
317
  * Ensure the standard image and the shared asdf data volume exist, and that the
310
318
  * agent CLIs are present inside the volume. Builds the image only when `docker
311
- * image inspect` fails. Best-effort: logs and continues on any error so the
312
- * host keeps booting.
319
+ * image inspect` fails or the content hash is stale. Never throws — returns
320
+ * `{ ok, error? }` so the caller decides (boot: fire-and-forget; taskUp:
321
+ * surface the reason).
313
322
  */
314
- export async function ensureStandardImage(): Promise<void> {
323
+ export async function ensureStandardImage(): Promise<StandardImageResult> {
315
324
  // 1. Shared asdf data volume — idempotent.
316
325
  const vol = await run("docker", ["volume", "create", ASDF_DATA_VOLUME]);
317
326
  if (vol.code !== 0) {
@@ -321,7 +330,9 @@ export async function ensureStandardImage(): Promise<void> {
321
330
  );
322
331
  // If docker itself is unavailable, the image step will also fail; bail
323
332
  // early so we don't double-log a confusing build error.
324
- if (vol.code === null) return;
333
+ if (vol.code === null) {
334
+ return { ok: false, error: "Docker is not available on this host." };
335
+ }
325
336
  }
326
337
 
327
338
  // 2. Ensure the image is built AND current. "Present" is not enough — a
@@ -330,6 +341,7 @@ export async function ensureStandardImage(): Promise<void> {
330
341
  // landed). The build context is content-hashed into an image label;
331
342
  // a mismatch triggers a rebuild (layer cache keeps it cheap).
332
343
  let imageReady = false;
344
+ let buildError: string | null = null;
333
345
  // Install only the optional engines the operator has configured; the flags
334
346
  // are build args AND part of the content hash (so a new login rebuilds).
335
347
  const engines = configuredOptionalEngines();
@@ -356,7 +368,7 @@ export async function ensureStandardImage(): Promise<void> {
356
368
  "[host-agent] docker unavailable; skipping standard image build. " +
357
369
  "Tasks will fail until docker is running.",
358
370
  );
359
- return;
371
+ return { ok: false, error: "Docker is not available on this host." };
360
372
  }
361
373
  const labeledHash = inspect.code === 0 ? inspect.stdout.trim() : null;
362
374
  if (inspect.code === 0 && contextHash !== null && labeledHash === contextHash) {
@@ -387,10 +399,13 @@ export async function ensureStandardImage(): Promise<void> {
387
399
  console.log(`[host-agent] built standard image ${STANDARD_IMAGE_TAG}`);
388
400
  imageReady = true;
389
401
  } else {
402
+ buildError =
403
+ build.stderr.trim() ||
404
+ `docker build exited ${build.code ?? "spawn error"}`;
390
405
  console.warn(
391
406
  `[host-agent] standard image build failed (exit ${build.code ?? "spawn"}); ` +
392
407
  "continuing. Tasks needing the image will surface this error.\n" +
393
- build.stderr.trim(),
408
+ buildError,
394
409
  );
395
410
  // A stale-but-working image is better than none.
396
411
  imageReady = labeledHash !== null;
@@ -402,5 +417,12 @@ export async function ensureStandardImage(): Promise<void> {
402
417
  if (imageReady) {
403
418
  await ensureVolumeAgentClis();
404
419
  await upgradeVolumeAgentClis();
420
+ return { ok: true };
405
421
  }
422
+ return {
423
+ ok: false,
424
+ error:
425
+ buildError ??
426
+ "the standard base image (uai-standard:dev) is not built and no build error was captured.",
427
+ };
406
428
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.8.25",
3
+ "version": "0.8.27",
4
4
  "description": "Uai host — runs ephemeral AI coding tasks in Docker on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Diogo Perillo <diogo.perillo@gmail.com>",
@@ -322,6 +322,11 @@ fi
322
322
  printf ' image: %s\n' "$DERIVED_IMAGE"
323
323
  else
324
324
  printf ' image: %s\n' "$STANDARD_IMAGE"
325
+ # Never pull: the standard image is built LOCALLY by the host, not hosted
326
+ # on any registry. Without this, an absent image makes compose attempt a
327
+ # doomed registry pull ("pull access denied for uai-standard") instead of
328
+ # failing clearly (live 2026-07-22, a fresh host mid standard-image build).
329
+ printf ' pull_policy: never\n'
325
330
  fi
326
331
  printf ' init: true\n'
327
332
  printf ' working_dir: /workspace\n'
@@ -441,6 +446,18 @@ fi
441
446
  docker volume create "$ASDF_VOLUME" >/dev/null 2>&1 || true
442
447
  docker volume create "$PW_VOLUME" >/dev/null 2>&1 || true
443
448
 
449
+ # The standard base image is built by the host (ensureStandardImage), never
450
+ # pulled from a registry — and derived tasks FROM it too. If it's absent (the
451
+ # host started before Docker was ready, or the build failed) compose would try
452
+ # a doomed registry pull; fail with an actionable message instead (the host
453
+ # also (re)builds it on demand before task-up — this is the belt-and-suspenders
454
+ # check for a still-in-progress or failed build). Live 2026-07-22.
455
+ if ! docker image inspect "$STANDARD_IMAGE" >/dev/null 2>&1; then
456
+ emit_err "STANDARD_IMAGE_MISSING" \
457
+ "the host base image $STANDARD_IMAGE is not built yet — the host builds it at startup (a few minutes on first run, and after Docker Desktop finishes starting). Wait for the host log line 'built standard image', or restart the host, then retry. If it keeps failing the host log shows the build error (often Docker Desktop low on memory/disk)." \
458
+ "check standard image"
459
+ fi
460
+
444
461
  step "COMPOSE_UP_FAILED" "docker compose up -d"
445
462
  if [ "$has_derived" = "1" ]; then
446
463
  docker compose -p "$compose_project" -f "$task_uai_dir/docker-compose.yml" up -d --build >/dev/null
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { agent, AgentError } from "../lib/agent";
1
+ import { agent, AgentError, toolStderrDetail } from "../lib/agent";
2
+ import { ensureStandardImage } from "../lib/standard-image";
2
3
  import { cloneRepo } from "../lib/repo-clone";
3
4
  import { handleFilesOp } from "../lib/shared-files";
4
5
  import { getOrchestrator } from "../lib/orchestrator";
@@ -119,7 +120,27 @@ export const hostCommands: HostCommands = {
119
120
  storeTaskCliSecret(input.task.id, input.task.cliSecret);
120
121
  }
121
122
  recordHostEvent(input.task.id, "task.created");
122
- const result = await wrapAgent(ctx, "taskUp", () => agent.taskUp(input));
123
+ const result = await wrapAgent(ctx, "taskUp", async () => {
124
+ // Self-heal the standard base image before task-up needs it. The
125
+ // boot-time build is best-effort + one-shot: a host that started before
126
+ // Docker was ready (common on Docker Desktop) never built
127
+ // uai-standard:dev, so every task-up died on a doomed registry pull
128
+ // (live 2026-07-22). This is idempotent — a fast inspect when present,
129
+ // a real build only when missing/stale (Docker is definitely up now, a
130
+ // task just arrived). On a build FAILURE, surface WHY to the cloud (not
131
+ // just the host log — the operator can't read a remote user's machine).
132
+ const img = await ensureStandardImage();
133
+ if (!img.ok) {
134
+ throw new AgentError(
135
+ "STANDARD_IMAGE_UNAVAILABLE",
136
+ img.error
137
+ ? `could not prepare the host base image uai-standard:dev — the build failed. ${toolStderrDetail(img.error)}`
138
+ : "could not prepare the host base image uai-standard:dev — see the host log.",
139
+ {},
140
+ );
141
+ }
142
+ return agent.taskUp(input);
143
+ });
123
144
  if (result.ok) {
124
145
  recordTaskUpResult(input.task.id, result.value);
125
146
  recordHostEvent(input.task.id, "task.started");