@runuai/host 0.9.65 → 0.9.67

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.
@@ -883,14 +883,21 @@ export async function detectContainerRuntime(
883
883
  appleUnavailable(preference, selectedAppleRuntime(deps) === null, deps)
884
884
  );
885
885
  }
886
- // ADR-106: prefer the bundled runtime wherever it can run — ambient
887
- // Docker existing is not a reason to skip it. A failed activation falls
888
- // through to Docker rather than degrading a machine that has one.
886
+ // ADR-119 (inverting ADR-106): a present, healthy ambient Docker/OrbStack
887
+ // daemon wins it is the battle-tested path, and `auto` must never
888
+ // migrate a machine off a working daemon it already trusts. The bundled
889
+ // runtime is the zero-dependency fallback for machines with no daemon:
890
+ // an absent/remote/unanswering Docker on a machine that never chose
891
+ // Docker is not an error to surface. One probe serves both outcomes —
892
+ // its rendered state is reused if the bundled runtime also fails.
893
+ const ambient = await detectAmbientDockerCore(preference, deps);
894
+ if (ambient.usable) return ambient.state;
889
895
  const selected = await probeAndSelectApple(preference, deps);
890
896
  if (selected !== null) return selected;
897
+ return ambient.state;
891
898
  }
892
899
 
893
- return detectAmbientDocker(preference, deps);
900
+ return (await detectAmbientDockerCore(preference, deps)).state;
894
901
  }
895
902
 
896
903
  function appleUnavailable(
@@ -915,10 +922,19 @@ function appleUnavailable(
915
922
  };
916
923
  }
917
924
 
918
- async function detectAmbientDocker(
925
+ /**
926
+ * Ambient-Docker detection. `usable: false` marks the ADR-119 fall-through
927
+ * verdicts — an absent daemon, a remote endpoint, an unresolvable context —
928
+ * so `auto` can try the bundled runtime instead of surfacing Docker's
929
+ * errors on a machine that never chose Docker, while still carrying the
930
+ * rendered state for callers that need it (explicit `docker`, and auto once
931
+ * the bundled runtime also failed). A ready selection and a pin conflict
932
+ * are both `usable: true`: neither may fall through.
933
+ */
934
+ async function detectAmbientDockerCore(
919
935
  preference: "auto" | "docker",
920
936
  deps: RuntimeDetectionDeps,
921
- ): Promise<ContainerRuntimeState> {
937
+ ): Promise<{ usable: boolean; state: ContainerRuntimeState }> {
922
938
  // A successful ambient `docker info` is not a durable selection: collapse a
923
939
  // named context to its exact local endpoint before publishing DOCKER_HOST.
924
940
  const selection =
@@ -929,24 +945,30 @@ async function detectAmbientDocker(
929
945
  : null;
930
946
  if (selection && !isLocalDockerEndpoint(selection.host)) {
931
947
  return {
932
- status: "no-runtime",
933
- preference,
934
- message:
935
- "The selected Docker endpoint is remote. " +
936
- "Uai Host requires a local Unix-socket daemon for workspace mounts and loopback tunnels. " +
937
- `Select a local Docker context, then run \`${RUNTIME_RESTART_COMMAND}\`.`,
938
- action: RUNTIME_RESTART_COMMAND,
948
+ usable: false,
949
+ state: {
950
+ status: "no-runtime",
951
+ preference,
952
+ message:
953
+ "The selected Docker endpoint is remote. " +
954
+ "Uai Host requires a local Unix-socket daemon for workspace mounts and loopback tunnels. " +
955
+ `Select a local Docker context, then run \`${RUNTIME_RESTART_COMMAND}\`.`,
956
+ action: RUNTIME_RESTART_COMMAND,
957
+ },
939
958
  };
940
959
  }
941
960
 
942
961
  if (deps.dockerInfo === undefined && selection === null) {
943
962
  return {
944
- status: "no-runtime",
945
- preference,
946
- message:
947
- "Uai Host could not resolve the selected Docker context to a local endpoint. " +
948
- `Select a local Docker context and run \`${RUNTIME_RECHECK_COMMAND}\`.`,
949
- action: RUNTIME_RECHECK_COMMAND,
963
+ usable: false,
964
+ state: {
965
+ status: "no-runtime",
966
+ preference,
967
+ message:
968
+ "Uai Host could not resolve the selected Docker context to a local endpoint. " +
969
+ `Select a local Docker context and run \`${RUNTIME_RECHECK_COMMAND}\`.`,
970
+ action: RUNTIME_RECHECK_COMMAND,
971
+ },
950
972
  };
951
973
  }
952
974
 
@@ -954,20 +976,26 @@ async function detectAmbientDocker(
954
976
  if (proof !== null) {
955
977
  if (selection) {
956
978
  if (!pinRuntime("docker", selection.host, proof.engineId)) {
957
- return pinnedRuntimeUnavailable(preference);
979
+ return { usable: true, state: pinnedRuntimeUnavailable(preference) };
958
980
  }
959
981
  }
960
- return { status: "ready", preference, provider: "docker" };
982
+ return {
983
+ usable: true,
984
+ state: { status: "ready", preference, provider: "docker" },
985
+ };
961
986
  }
962
987
  return {
963
- status: "no-runtime",
964
- preference,
965
- message:
966
- preference === "docker"
967
- ? "No working Docker runtime was found. Install or start Docker, then run " +
968
- `\`${RUNTIME_RECHECK_COMMAND}\`.`
969
- : NO_RUNTIME_MESSAGE,
970
- action: RUNTIME_RECHECK_COMMAND,
988
+ usable: false,
989
+ state: {
990
+ status: "no-runtime",
991
+ preference,
992
+ message:
993
+ preference === "docker"
994
+ ? "No working Docker runtime was found. Install or start Docker, then run " +
995
+ `\`${RUNTIME_RECHECK_COMMAND}\`.`
996
+ : NO_RUNTIME_MESSAGE,
997
+ action: RUNTIME_RECHECK_COMMAND,
998
+ },
971
999
  };
972
1000
  }
973
1001
 
@@ -6199,6 +6199,31 @@ export async function recoverAppleTaskEnvironment(
6199
6199
  );
6200
6200
  return settleFromStatus(after);
6201
6201
  }
6202
+ if (init.exitCode === 127) {
6203
+ // Command-not-found from a read-only bind mount the guest cannot touch:
6204
+ // the initializer itself is unrunnable — live 2026-08-26, containers
6205
+ // whose versioned asset tree a host update had pruned looped here as
6206
+ // "recovered with warnings" while the host sat in checking and the UI
6207
+ // showed Running (so no Resume). An unrunnable initializer is a stopped
6208
+ // task, not a degraded one: stop, settle the durable verdict, and let
6209
+ // Resume recreate the container against current (task-scoped) assets.
6210
+ await environment.stop();
6211
+ const after = await environment.status();
6212
+ if (after.state === "running" || after.state === "unknown") {
6213
+ return {
6214
+ outcome: "deferred",
6215
+ detail:
6216
+ "task initializer is unrunnable (exit 127) and the container could not be proven stopped",
6217
+ };
6218
+ }
6219
+ if (!context.isCurrent()) {
6220
+ return { outcome: "deferred", detail: "runtime generation superseded" };
6221
+ }
6222
+ console.error(
6223
+ `[orchestrator] recovery: ${descriptor.taskId} initializer is unrunnable (exit 127 — usually a host update pruned the versioned assets this pre-0.9.67 container still mounts); task was stopped, Resume recreates it`,
6224
+ );
6225
+ return settleFromStatus(after);
6226
+ }
6202
6227
  if (init.exitCode !== 0) {
6203
6228
  console.warn(
6204
6229
  `[orchestrator] recovery: ${descriptor.taskId} uai-init failed (exit ${init.exitCode}); container is up but Editor may be down`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.65",
3
+ "version": "0.9.67",
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>",
@@ -1374,6 +1374,48 @@ if [ "$project_count" -gt 0 ]; then
1374
1374
  fi
1375
1375
  fi
1376
1376
 
1377
+ # Containers bind these assets for their whole life, but the packaged
1378
+ # sources live in the VERSIONED runtime tree and the updater keeps only
1379
+ # current+previous — a container surviving two updates found its mounts
1380
+ # pointing at deleted files (uai-init exec 127, recovery wedged the host in
1381
+ # "checking"; live 2026-08-26). Copy the assets into the task's own control
1382
+ # tree and mount THOSE: asset lifetime becomes task lifetime, and a Resume
1383
+ # refreshes the copies to the current version. Atomic rename so a resumed
1384
+ # container can never observe a half-written initializer.
1385
+ task_assets_dir="$task_uai_dir/assets"
1386
+ mkdir -p "$task_assets_dir"
1387
+ copy_task_asset() {
1388
+ copy_src="$1"
1389
+ copy_name="$2"
1390
+ copy_mode="$3"
1391
+ copy_tmp=$(mktemp "$task_assets_dir/.$copy_name.XXXXXX") || return 1
1392
+ cp "$copy_src" "$copy_tmp" || { rm -f "$copy_tmp"; return 1; }
1393
+ chmod "$copy_mode" "$copy_tmp" || { rm -f "$copy_tmp"; return 1; }
1394
+ mv -f "$copy_tmp" "$task_assets_dir/$copy_name"
1395
+ }
1396
+ if ! copy_task_asset "$UAI_INIT_SOURCE" uai-init 0755; then
1397
+ emit_err "CONTAINER_INIT_FAILED" \
1398
+ "Uai could not stage the task initializer into this task's control tree. Check disk space and permissions under the Uai data directory, then retry." \
1399
+ "stage task container assets"
1400
+ fi
1401
+ UAI_INIT_SOURCE="$task_assets_dir/uai-init"
1402
+ if [ -f "$RUNTIME_MATERIALIZER_SOURCE" ]; then
1403
+ if ! copy_task_asset "$RUNTIME_MATERIALIZER_SOURCE" uai-materialize-runtimes 0755; then
1404
+ emit_err "CONTAINER_INIT_FAILED" \
1405
+ "Uai could not stage the runtime materializer into this task's control tree. Check disk space and permissions under the Uai data directory, then retry." \
1406
+ "stage task container assets"
1407
+ fi
1408
+ RUNTIME_MATERIALIZER_SOURCE="$task_assets_dir/uai-materialize-runtimes"
1409
+ fi
1410
+ if [ -f "$COREPACK_VERSION_SOURCE" ]; then
1411
+ if ! copy_task_asset "$COREPACK_VERSION_SOURCE" corepack-version 0644; then
1412
+ emit_err "CONTAINER_INIT_FAILED" \
1413
+ "Uai could not stage the Corepack version pin into this task's control tree. Check disk space and permissions under the Uai data directory, then retry." \
1414
+ "stage task container assets"
1415
+ fi
1416
+ COREPACK_VERSION_SOURCE="$task_assets_dir/corepack-version"
1417
+ fi
1418
+
1377
1419
  # The host-authored allowlist is the single project-set authority shared by
1378
1420
  # pre-start runtime proof and task-local dependency installation. It lives in
1379
1421
  # the container-unmounted .uai control tree and is exposed only as this one