@runuai/host 0.9.60 → 0.9.62

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.
@@ -169,7 +169,11 @@ export interface EngineLoginManagerOptions {
169
169
  seams?: Partial<EngineLoginSeams>;
170
170
  }
171
171
 
172
- type OperationStage = "starting" | "awaiting_input" | "awaiting_callback";
172
+ type OperationStage =
173
+ | "starting"
174
+ | "awaiting_input"
175
+ | "awaiting_callback"
176
+ | "awaiting_confirmation";
173
177
 
174
178
  interface CodexCallbackTarget {
175
179
  readonly origin: string;
@@ -819,6 +823,25 @@ export class EngineLoginManager {
819
823
  }
820
824
 
821
825
  if (operation.stage === "starting") {
826
+ const device = findCodexDeviceAuthorization(operation.outputTail);
827
+ if (device) {
828
+ console.log(
829
+ `[engine-login] codex device authorization found for ${operation.opId}; awaiting operator confirmation`,
830
+ );
831
+ operation.stage = "awaiting_confirmation";
832
+ safeEmit(
833
+ operation.emit,
834
+ authorizeEvent(operation, device.verificationUrl),
835
+ );
836
+ safeEmit(operation.emit, {
837
+ kind: "engine.login.event",
838
+ opId: operation.opId,
839
+ engine: operation.engine,
840
+ phase: "awaiting_confirmation",
841
+ message: `Enter code ${device.userCode} on the verification page. Nothing to paste here — this completes on its own.`,
842
+ });
843
+ return;
844
+ }
822
845
  const authorization = findCodexAuthorization(operation.outputTail);
823
846
  if (authorization) {
824
847
  // Symmetric with the claude branch: this breadcrumb's ABSENCE was
@@ -1489,6 +1512,24 @@ export function findSafeHttpsUrl(value: string): string | null {
1489
1512
  return null;
1490
1513
  }
1491
1514
 
1515
+ /**
1516
+ * Codex `--device-auth` banner: a verification URL plus a short one-time
1517
+ * code ("AC57-MDV9G" shape). Both the phrase gate and the code shape are
1518
+ * required so container names, hashes, or URL fragments cannot fake a
1519
+ * device prompt. Reads the merged tail — codex prints to stderr.
1520
+ */
1521
+ export function findCodexDeviceAuthorization(
1522
+ value: string,
1523
+ ): { verificationUrl: string; userCode: string } | null {
1524
+ const plain = stripTerminalControl(value);
1525
+ if (!/one-time code|device code/i.test(plain)) return null;
1526
+ const verificationUrl = findSafeHttpsUrl(plain);
1527
+ if (!verificationUrl) return null;
1528
+ const code = /(?:^|\s)([A-Z0-9]{4,8}-[A-Z0-9]{4,10})(?:\s|$)/m.exec(plain);
1529
+ if (!code) return null;
1530
+ return { verificationUrl, userCode: code[1]! };
1531
+ }
1532
+
1492
1533
  export function extractClaudeOAuthToken(value: string): string | null {
1493
1534
  const plain = stripTerminalControl(value);
1494
1535
  // The FULL `sk-ant-oat01-` prefix and a realistic minimum length are both
@@ -1921,7 +1962,12 @@ export function engineLoginContainerArgs(
1921
1962
  // because the typescript went to /dev/null.
1922
1963
  `${LOGIN_MOUNT}/typescript`,
1923
1964
  ]
1924
- : ["codex", "login"];
1965
+ : // Device-code flow: no localhost callback, no paste — the CLI prints
1966
+ // a verification URL + one-time code, the operator confirms on the
1967
+ // provider's page, and the CLI polls to completion (ADR-116 v2,
1968
+ // 2026-08-24). The callback machinery below stays as the fallback
1969
+ // for CLIs that still print the localhost authorize URL.
1970
+ ["codex", "login", "--device-auth"];
1925
1971
  if (engineLoginBackend().apple) {
1926
1972
  // Same containment contract, Apple dialect: read-only root, all caps
1927
1973
  // dropped, a plain tmpfs at /tmp (the Apple CLI takes no mount options —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.60",
3
+ "version": "0.9.62",
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>",
package/src/index.ts CHANGED
@@ -168,7 +168,68 @@ export const hostEvents: HostEventStream = {
168
168
  },
169
169
  };
170
170
 
171
+ /** Set by main.ts at boot: the drain-then-supervisor-respawn used for
172
+ * staged activation. A hook (not an import) because main imports this
173
+ * module. */
174
+ let managedHostRestartHook: (() => void) | null = null;
175
+ export function setManagedHostRestartHook(hook: () => void): void {
176
+ managedHostRestartHook = hook;
177
+ }
178
+
171
179
  export const hostCommands: HostCommands = {
180
+ async hostUpdate(ctx) {
181
+ logCommand(ctx, "hostUpdate");
182
+ try {
183
+ const { updateManagedRuntime } = await import("../lib/managed-runtime");
184
+ const outcome = await updateManagedRuntime();
185
+ if (outcome.status === "not-managed") {
186
+ return {
187
+ ok: false,
188
+ code: HostErrorCode.Internal,
189
+ message:
190
+ "this host is not a managed install — update it where it runs",
191
+ retryable: false,
192
+ };
193
+ }
194
+ if (outcome.status === "deferred") {
195
+ // A rollback hold is an operator verdict that the newer release is
196
+ // unsafe on this host; a remote click must not override it.
197
+ return {
198
+ ok: true,
199
+ value: { version: outcome.version, staged: false, deferred: true },
200
+ };
201
+ }
202
+ if (outcome.status === "current") {
203
+ return { ok: true, value: { version: outcome.version, staged: false } };
204
+ }
205
+ managedHostRestartHook?.();
206
+ return { ok: true, value: { version: outcome.version, staged: true } };
207
+ } catch (error) {
208
+ return {
209
+ ok: false,
210
+ code: HostErrorCode.Internal,
211
+ message: `update failed: ${
212
+ error instanceof Error ? error.message : "unknown error"
213
+ }`,
214
+ retryable: true,
215
+ };
216
+ }
217
+ },
218
+ async hostRestart(ctx) {
219
+ logCommand(ctx, "hostRestart");
220
+ if (!managedHostRestartHook) {
221
+ return {
222
+ ok: false,
223
+ code: HostErrorCode.Internal,
224
+ message: "restart is unavailable in this process",
225
+ retryable: false,
226
+ };
227
+ }
228
+ // The result frame flushes before exit: shutdown drains in-flight
229
+ // commands (this one included) before the process terminates.
230
+ managedHostRestartHook();
231
+ return { ok: true, value: undefined };
232
+ },
172
233
  async taskUp(ctx, input) {
173
234
  logCommand(ctx, "taskUp", input.task.id);
174
235
  const admissionFailure = managedMaintenanceUnavailable();
package/src/main.ts CHANGED
@@ -161,7 +161,7 @@ import {
161
161
  onAgentClisReady,
162
162
  standardRuntimes,
163
163
  } from "../lib/standard-image";
164
- import { hostCommands, hostEvents } from "./index";
164
+ import { hostCommands, hostEvents, setManagedHostRestartHook } from "./index";
165
165
  import { EventOutbox, transmitEventOutbox } from "./event-outbox";
166
166
  import {
167
167
  HostErrorCode,
@@ -171,6 +171,7 @@ import {
171
171
  GITHUB_REPOSITORY_ACCESS_PROTOCOL_FEATURE,
172
172
  HOST_MAINTENANCE_READINESS_PROTOCOL_FEATURE,
173
173
  HOST_CONFIG_PROTOCOL_FEATURE,
174
+ HOST_REMOTE_LIFECYCLE_PROTOCOL_FEATURE,
174
175
  HOST_ENGINE_ACCOUNTS_PROTOCOL_FEATURE,
175
176
  HOST_ENGINE_CONNECT_PROTOCOL_FEATURE,
176
177
  HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
@@ -567,6 +568,7 @@ function buildCapabilities(): HostCapabilities {
567
568
  HOST_LOGS_PROTOCOL_FEATURE,
568
569
  HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
569
570
  HOST_CONFIG_PROTOCOL_FEATURE,
571
+ HOST_REMOTE_LIFECYCLE_PROTOCOL_FEATURE,
570
572
  HOST_ENGINE_ACCOUNTS_PROTOCOL_FEATURE,
571
573
  HOST_ENGINE_CONNECT_PROTOCOL_FEATURE,
572
574
  // The echo adapter cannot execute the in-task CLI. Advertising typed
@@ -2284,6 +2286,10 @@ async function dispatchCommand(
2284
2286
  expectString(args, 1),
2285
2287
  expectNumberArg(args, 2),
2286
2288
  );
2289
+ case "hostUpdate":
2290
+ return hostCommands.hostUpdate(ctx);
2291
+ case "hostRestart":
2292
+ return hostCommands.hostRestart(ctx);
2287
2293
  case "engineAccountRemove": {
2288
2294
  // ADR-116: mirror the local API's post-mutation behavior — the cloud's
2289
2295
  // account list self-heals through the capability re-advertisement.
@@ -3263,6 +3269,8 @@ export function requestManagedHostRestart(): void {
3263
3269
  shutdownExitCode = MANAGED_UPDATE_RESTART_EXIT_CODE;
3264
3270
  requestShutdown("SIGTERM");
3265
3271
  }
3272
+ // Remote hostUpdate/hostRestart commands reuse the exact activation restart.
3273
+ setManagedHostRestartHook(requestManagedHostRestart);
3266
3274
 
3267
3275
  /** Automatic host updates wait only for in-flight work to drain. Running
3268
3276
  * tasks are deliberately not counted: durable sessions survive the service
package/src/protocol.ts CHANGED
@@ -86,6 +86,9 @@ export const MCP_GATEWAY_HEALTH_PROTOCOL_FEATURE = "mcp-gateway-health-v1";
86
86
  export const HOST_LOGS_PROTOCOL_FEATURE = "host-logs-v1";
87
87
  export const HOST_ENGINE_LOGIN_PROTOCOL_FEATURE = "host-engine-login-v1";
88
88
  export const HOST_CONFIG_PROTOCOL_FEATURE = "host-config-v1";
89
+ /** Remote update/restart over the bridge (owner-gated cloud routes). */
90
+ export const HOST_REMOTE_LIFECYCLE_PROTOCOL_FEATURE =
91
+ "host-remote-lifecycle-v1";
89
92
  /** ADR-116: labeled engine accounts on the cloud host page — account list in
90
93
  * capabilities, account-targeted login (`label` on engine.login.start), and
91
94
  * the engineAccountRemove command. */
@@ -345,6 +348,9 @@ export type EngineLoginEventFrame =
345
348
  | "starting"
346
349
  | "awaiting_input"
347
350
  | "awaiting_callback"
351
+ // Device-code flow (codex `--device-auth`): the operator confirms a
352
+ // short one-time code on the provider's page; nothing is pasted back.
353
+ | "awaiting_confirmation"
348
354
  | "succeeded"
349
355
  | "cancelled";
350
356
  url?: never;
@@ -787,6 +793,7 @@ export function isEngineLoginEventFrame(
787
793
  (frame.phase === "starting" ||
788
794
  frame.phase === "awaiting_input" ||
789
795
  frame.phase === "awaiting_callback" ||
796
+ frame.phase === "awaiting_confirmation" ||
790
797
  frame.phase === "succeeded" ||
791
798
  frame.phase === "cancelled") &&
792
799
  !Object.hasOwn(frame, "errorCode")
@@ -1246,7 +1253,26 @@ export type FilesOpValue =
1246
1253
  | { dataBase64: string; size: number; eof: boolean } // read
1247
1254
  | Record<string, never>; // write / mkdir / delete
1248
1255
 
1256
+ export interface HostUpdateOutcome {
1257
+ version: string;
1258
+ /** True when a newer signed release was staged and the host is about to
1259
+ * drain-restart into it; false when it was already current. */
1260
+ staged: boolean;
1261
+ /** Set when a rollback hold deferred the update (operator ran a rollback;
1262
+ * remote updates respect that verdict). */
1263
+ deferred?: boolean;
1264
+ }
1265
+
1249
1266
  export interface HostCommands {
1267
+ /**
1268
+ * Stage a signed update from the release feed and drain-restart into it.
1269
+ * Managed installs only; the host verifies the signature itself, so the
1270
+ * remote trigger adds no trust surface. The reconnect on the new version
1271
+ * is the real confirmation.
1272
+ */
1273
+ hostUpdate(ctx: CommandContext): Promise<HostCommandResult<HostUpdateOutcome>>;
1274
+ /** Drain in-flight work, then let the service supervisor respawn the host. */
1275
+ hostRestart(ctx: CommandContext): Promise<HostCommandResult<void>>;
1250
1276
  taskUp(
1251
1277
  ctx: CommandContext,
1252
1278
  input: TaskLaunchInput,