@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.
- package/lib/standard-image.ts +28 -6
- package/package.json +1 -1
- package/scripts/agent/task-up.sh +17 -0
- package/src/index.ts +23 -2
package/lib/standard-image.ts
CHANGED
|
@@ -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
|
|
312
|
-
*
|
|
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<
|
|
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)
|
|
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
|
-
|
|
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
package/scripts/agent/task-up.sh
CHANGED
|
@@ -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", () =>
|
|
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");
|