@runuai/host 0.8.26 → 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.26",
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>",
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { agent, AgentError } from "../lib/agent";
1
+ import { agent, AgentError, toolStderrDetail } from "../lib/agent";
2
2
  import { ensureStandardImage } from "../lib/standard-image";
3
3
  import { cloneRepo } from "../lib/repo-clone";
4
4
  import { handleFilesOp } from "../lib/shared-files";
@@ -120,16 +120,27 @@ export const hostCommands: HostCommands = {
120
120
  storeTaskCliSecret(input.task.id, input.task.cliSecret);
121
121
  }
122
122
  recordHostEvent(input.task.id, "task.created");
123
- // Self-heal the standard base image before task-up needs it. The boot-time
124
- // build is best-effort + one-shot: if the host started before Docker was
125
- // ready (common on Docker Desktop), uai-standard:dev was never built and
126
- // every task-up died on a doomed registry pull (live 2026-07-22). This is
127
- // idempotent a fast image-inspect when present, a real build only when
128
- // missing/stale (Docker is definitely up now, a task just arrived). Never
129
- // throws; a build failure surfaces as a clear STANDARD_IMAGE_MISSING from
130
- // task-up.sh's guard.
131
- await ensureStandardImage();
132
- 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
+ });
133
144
  if (result.ok) {
134
145
  recordTaskUpResult(input.task.id, result.value);
135
146
  recordHostEvent(input.task.id, "task.started");