@runuai/host 0.9.104 → 0.9.106

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.
@@ -68,7 +68,17 @@ const KINDS: Record<string, KindDescriptor> = {
68
68
  // API-key engines: an extra account is one env var. The default slot stays
69
69
  // the host's own `.env.local` key (the adapters read process.env), so these
70
70
  // never synthesize a default — `defaultAvailable` is false for them.
71
- grok: { authKind: "env" },
71
+ // Grok: a subscription sign-in is a config dir (`grok login --device-auth`
72
+ // writes auth.json; GROK_HOME selects it in-container). Pasted xAI keys
73
+ // remain `env` rows (XAI_API_KEY) — rows carry their own authKind.
74
+ grok: {
75
+ authKind: "config-dir",
76
+ defaultHostDir: (owner) => join(owner, ".grok"),
77
+ defaultContainerDir: "/home/node/.grok",
78
+ selectVar: "GROK_HOME",
79
+ containerDirPrefix: "/home/node/.grok-acct-",
80
+ extraHostDir: (owner, id) => join(owner, `.grok-acct-${id}`),
81
+ },
72
82
  cursor: { authKind: "env" },
73
83
  codex: {
74
84
  authKind: "config-dir",
@@ -838,6 +848,28 @@ export function addCodexAccountFromAuthJson(
838
848
  seams: Partial<AccountSeams> = {},
839
849
  scope: string = HOST_ACCOUNT_SCOPE,
840
850
  ): AddAccountResult {
851
+ return addConfigDirAccountFromAuthJson("codex", label, authJson, seams, scope);
852
+ }
853
+
854
+ /** Grok (host 0.9.105): the `auth.json` a `grok login --device-auth` capture
855
+ * produced, stored as an isolated `~/.grok-acct-<id>` selected by GROK_HOME. */
856
+ export function addGrokAccountFromAuthJson(
857
+ label: string,
858
+ authJson: string,
859
+ seams: Partial<AccountSeams> = {},
860
+ scope: string = HOST_ACCOUNT_SCOPE,
861
+ ): AddAccountResult {
862
+ return addConfigDirAccountFromAuthJson("grok", label, authJson, seams, scope);
863
+ }
864
+
865
+ function addConfigDirAccountFromAuthJson(
866
+ kind: "codex" | "grok",
867
+ label: string,
868
+ authJson: string,
869
+ seams: Partial<AccountSeams>,
870
+ scope: string,
871
+ ): AddAccountResult {
872
+ const engineLabel = kind === "codex" ? "Codex" : "Grok";
841
873
  if (!isEngineAccountScope(scope)) {
842
874
  return { ok: false, message: "The account scope is invalid." };
843
875
  }
@@ -850,11 +882,11 @@ export function addCodexAccountFromAuthJson(
850
882
  throw new Error("not an object");
851
883
  }
852
884
  } catch {
853
- return { ok: false, message: "The captured Codex credential is not valid." };
885
+ return { ok: false, message: `The captured ${engineLabel} credential is not valid.` };
854
886
  }
855
887
 
856
888
  const id = newId();
857
- const d = KINDS.codex as KindDescriptor;
889
+ const d = KINDS[kind] as KindDescriptor;
858
890
  const hostDir = (d.extraHostDir as (o: string, i: string) => string)(
859
891
  s.ownerHome(),
860
892
  id,
@@ -882,17 +914,17 @@ export function addCodexAccountFromAuthJson(
882
914
  .insert(schema.engineAccounts)
883
915
  .values({
884
916
  id,
885
- kind: "codex",
917
+ kind,
886
918
  label: trimmedLabel,
887
919
  authKind: "config-dir",
888
920
  secretEnc: null,
889
921
  configDir: hostDir,
890
922
  createdAt: Date.now(),
891
923
  scope,
892
- position: nextPosition("codex", scope),
924
+ position: nextPosition(kind, scope),
893
925
  })
894
926
  .run();
895
- return { ok: true, message: `Added Codex account "${trimmedLabel}".`, id };
927
+ return { ok: true, message: `Added ${engineLabel} account "${trimmedLabel}".`, id };
896
928
  }
897
929
 
898
930
  /** Remove an EXTRA account (idempotent). The synthesized default can't be
@@ -42,6 +42,7 @@ import {
42
42
  import {
43
43
  addCodexAccountFromAuthJson,
44
44
  addEngineAccount,
45
+ addGrokAccountFromAuthJson,
45
46
  } from "./engine-accounts";
46
47
  import { reinjectCodexRunningTasks } from "./codex-auth";
47
48
  import {
@@ -67,6 +68,16 @@ export const MAX_CONCURRENT_ENGINE_LOGINS = 1;
67
68
  const LOGIN_MOUNT = "/run/uai-engine-login";
68
69
  const LOGIN_HOME = `${LOGIN_MOUNT}/home`;
69
70
  const CODEX_HOME = `${LOGIN_MOUNT}/codex`;
71
+ /** grok writes its auth.json under GROK_HOME (host 0.9.105 remote sign-in). */
72
+ const GROK_HOME = `${LOGIN_MOUNT}/grok`;
73
+ /** The login image only carries grok when the host had it configured at its
74
+ * last image build; a shared host never did. Install into the op's private
75
+ * HOME on the fly (x.ai's installer, ~seconds) when the image lacks it. */
76
+ const GROK_LOGIN_SHELL =
77
+ 'g="$(command -v grok 2>/dev/null || true)"; ' +
78
+ '[ -n "$g" ] || { curl -fsSL https://x.ai/cli/install.sh | bash >/dev/null 2>&1; g="$HOME/.grok/bin/grok"; }; ' +
79
+ '[ -x "$g" ] || { echo "grok CLI is not available in the login image" >&2; exit 127; }; ' +
80
+ 'exec "$g" login --device-auth';
70
81
  const MAX_OUTPUT_TAIL_BYTES = 64 * 1_024;
71
82
  /** script(1) transcripts of a login run a few KB; 1MB is a hostile-file cap. */
72
83
  const MAX_TRANSCRIPT_BYTES = 1_024 * 1_024;
@@ -157,6 +168,15 @@ export interface EngineLoginSeams {
157
168
  authJson: string,
158
169
  scope: string,
159
170
  ): { ok: boolean; message: string; id?: string };
171
+ addGrokAccount(
172
+ label: string,
173
+ authJson: string,
174
+ scope: string,
175
+ ): { ok: boolean; message: string; id?: string };
176
+ persistGrokAuth(sourceFile: string): Promise<void>;
177
+ /** Optional engines ride the standard image only when configured; a fresh
178
+ * account may be the first configuration → rebuild (queue-deduped). */
179
+ ensureStandardImage(): Promise<void>;
160
180
  refreshEngineAccountAgents(engine: "claude"): Promise<void>;
161
181
  reinjectCodexRunningTasks(): Promise<void>;
162
182
  ownerHome(): string;
@@ -177,6 +197,8 @@ export interface EngineLoginManagerOptions {
177
197
  /** Defer Docker inventory until the first start when no runtime is selected. */
178
198
  reconcileOnCreate?: boolean;
179
199
  seams?: Partial<EngineLoginSeams>;
200
+ /** Rebuild the standard image after a successful capture (optional engines). */
201
+ ensureStandardImage?: () => Promise<unknown>;
180
202
  }
181
203
 
182
204
  type OperationStage =
@@ -289,6 +311,26 @@ export async function persistCodexAuthAtomic(
289
311
  );
290
312
  }
291
313
 
314
+ export async function persistGrokAuthAtomic(
315
+ sourceFile: string,
316
+ options: { ownerHome?: string } = {},
317
+ ): Promise<void> {
318
+ const body = await readBoundedRegularFile(sourceFile, MAX_CODEX_AUTH_BYTES);
319
+ let parsed: unknown;
320
+ try {
321
+ parsed = JSON.parse(body.toString("utf8"));
322
+ } catch {
323
+ throw new Error("Grok produced invalid auth data");
324
+ }
325
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
326
+ throw new Error("Grok produced invalid auth data");
327
+ }
328
+ await writeSensitiveFileAtomic(
329
+ join(options.ownerHome ?? ownerHome(), ".grok", "auth.json"),
330
+ body,
331
+ );
332
+ }
333
+
292
334
  function defaultSeams(options: EngineLoginManagerOptions): EngineLoginSeams {
293
335
  const tempRoot = options.tempRoot;
294
336
  return {
@@ -315,6 +357,12 @@ function defaultSeams(options: EngineLoginManagerOptions): EngineLoginSeams {
315
357
  addEngineAccount("claude", label, { token }, {}, scope),
316
358
  addCodexAccount: (label, authJson, scope) =>
317
359
  addCodexAccountFromAuthJson(label, authJson, {}, scope),
360
+ addGrokAccount: (label, authJson, scope) =>
361
+ addGrokAccountFromAuthJson(label, authJson, {}, scope),
362
+ persistGrokAuth: (sourceFile) => persistGrokAuthAtomic(sourceFile),
363
+ ensureStandardImage: async () => {
364
+ await options.ensureStandardImage?.();
365
+ },
318
366
  refreshEngineAccountAgents: options.refreshEngineAccountAgents,
319
367
  reinjectCodexRunningTasks: () => reinjectCodexRunningTasks(),
320
368
  ownerHome,
@@ -362,6 +410,9 @@ export class EngineLoginManager {
362
410
  const codexConfigured = this.seams.fileExists(
363
411
  join(this.seams.ownerHome(), ".codex", "auth.json"),
364
412
  );
413
+ const grokConfigured =
414
+ this.seams.fileExists(join(this.seams.ownerHome(), ".grok", "auth.json")) ||
415
+ Boolean(this.seams.procEnv.XAI_API_KEY);
365
416
  return [
366
417
  {
367
418
  engine: "claude",
@@ -373,6 +424,11 @@ export class EngineLoginManager {
373
424
  status: codexConfigured ? "configured" : "missing",
374
425
  loginSupported: true,
375
426
  },
427
+ {
428
+ engine: "grok",
429
+ status: grokConfigured ? "configured" : "missing",
430
+ loginSupported: true,
431
+ },
376
432
  ];
377
433
  }
378
434
 
@@ -853,10 +909,13 @@ export class EngineLoginManager {
853
909
  }
854
910
 
855
911
  if (operation.stage === "starting") {
856
- const device = findCodexDeviceAuthorization(operation.outputTail);
912
+ const device =
913
+ operation.engine === "grok"
914
+ ? findGrokDeviceAuthorization(operation.outputTail)
915
+ : findCodexDeviceAuthorization(operation.outputTail);
857
916
  if (device) {
858
917
  console.log(
859
- `[engine-login] codex device authorization found for ${operation.opId}; awaiting operator confirmation`,
918
+ `[engine-login] ${operation.engine} device authorization found for ${operation.opId}; awaiting operator confirmation`,
860
919
  );
861
920
  operation.stage = "awaiting_confirmation";
862
921
  safeEmit(
@@ -873,6 +932,7 @@ export class EngineLoginManager {
873
932
  });
874
933
  return;
875
934
  }
935
+ if (operation.engine !== "codex") return;
876
936
  const authorization = findCodexAuthorization(operation.outputTail);
877
937
  if (authorization) {
878
938
  // Symmetric with the claude branch: this breadcrumb's ABSENCE was
@@ -903,7 +963,10 @@ export class EngineLoginManager {
903
963
  result: EngineLoginProcessResult,
904
964
  ): Promise<void> {
905
965
  if (!this.isCurrent(operation) || operation.finishing) return;
906
- if (operation.engine === "codex" && result.code === 0) {
966
+ if (
967
+ (operation.engine === "codex" || operation.engine === "grok") &&
968
+ result.code === 0
969
+ ) {
907
970
  this.beginCodexFinish(operation);
908
971
  if (operation.finishPromise) await operation.finishPromise;
909
972
  return;
@@ -1037,25 +1100,34 @@ export class EngineLoginManager {
1037
1100
  private async commitCodex(operation: LoginOperation): Promise<void> {
1038
1101
  const tempDir = operation.tempDir;
1039
1102
  if (!tempDir) return;
1103
+ // codex and grok share the shape: a device-code CLI that exits 0 after
1104
+ // writing auth.json under its HOME var (CODEX_HOME / GROK_HOME), which
1105
+ // the container mounts from the op's private leaf.
1106
+ const grok = operation.engine === "grok";
1107
+ const authFile = join(tempDir, grok ? "grok" : "codex", "auth.json");
1108
+ const label = grok ? "Grok" : "Codex";
1040
1109
  let terminal: EngineLoginEventFrame;
1041
1110
  if (operation.accountLabel !== null) {
1042
1111
  // ADR-116: labeled capture — the auth.json this login produced becomes
1043
1112
  // an isolated extra account instead of overwriting ~/.codex.
1044
1113
  let added: { ok: boolean; message: string; id?: string };
1045
1114
  try {
1046
- const contents = await readFile(
1047
- join(tempDir, "codex", "auth.json"),
1048
- "utf8",
1049
- );
1050
- added = this.seams.addCodexAccount(
1051
- operation.accountLabel,
1052
- contents,
1053
- operation.accountScope,
1054
- );
1115
+ const contents = await readFile(authFile, "utf8");
1116
+ added = grok
1117
+ ? this.seams.addGrokAccount(
1118
+ operation.accountLabel,
1119
+ contents,
1120
+ operation.accountScope,
1121
+ )
1122
+ : this.seams.addCodexAccount(
1123
+ operation.accountLabel,
1124
+ contents,
1125
+ operation.accountScope,
1126
+ );
1055
1127
  } catch {
1056
1128
  added = {
1057
1129
  ok: false,
1058
- message: "Codex authorized, but its credential could not be read.",
1130
+ message: `${label} authorized, but its credential could not be read.`,
1059
1131
  };
1060
1132
  }
1061
1133
  terminal = added.ok
@@ -1074,31 +1146,32 @@ export class EngineLoginManager {
1074
1146
  added.message,
1075
1147
  );
1076
1148
  await this.finishCommit(operation, terminal);
1149
+ if (added.ok) runBestEffort(() => this.seams.ensureStandardImage());
1077
1150
  return;
1078
1151
  }
1079
1152
  try {
1080
- await this.seams.persistCodexAuth(
1081
- join(tempDir, "codex", "auth.json"),
1082
- );
1153
+ if (grok) await this.seams.persistGrokAuth(authFile);
1154
+ else await this.seams.persistCodexAuth(authFile);
1083
1155
  } catch {
1084
1156
  terminal = failedEvent(
1085
1157
  operation.opId,
1086
1158
  operation.engine,
1087
1159
  "persist_failed",
1088
- "Codex authorized, but its credential could not be saved.",
1160
+ `${label} authorized, but its credential could not be saved.`,
1089
1161
  );
1090
1162
  await this.finishCommit(operation, terminal);
1091
1163
  return;
1092
1164
  }
1093
- runBestEffort(() => this.seams.reinjectCodexRunningTasks());
1165
+ if (!grok) runBestEffort(() => this.seams.reinjectCodexRunningTasks());
1094
1166
  terminal = {
1095
1167
  kind: "engine.login.event",
1096
1168
  opId: operation.opId,
1097
1169
  engine: operation.engine,
1098
1170
  phase: "succeeded",
1099
- message: "Codex connected.",
1171
+ message: `${label} connected.`,
1100
1172
  };
1101
1173
  await this.finishCommit(operation, terminal);
1174
+ runBestEffort(() => this.seams.ensureStandardImage());
1102
1175
  }
1103
1176
 
1104
1177
  private async finishCommit(
@@ -1319,8 +1392,8 @@ function safeEmit(
1319
1392
  }
1320
1393
  }
1321
1394
 
1322
- function engineLabel(engine: EngineLoginKind): "Claude" | "Codex" {
1323
- return engine === "claude" ? "Claude" : "Codex";
1395
+ function engineLabel(engine: EngineLoginKind): "Claude" | "Codex" | "Grok" {
1396
+ return engine === "claude" ? "Claude" : engine === "codex" ? "Codex" : "Grok";
1324
1397
  }
1325
1398
 
1326
1399
  function failedEvent(
@@ -1571,6 +1644,26 @@ export function findCodexDeviceAuthorization(
1571
1644
  return { verificationUrl, userCode: code[1]! };
1572
1645
  }
1573
1646
 
1647
+ /**
1648
+ * Grok `login --device-auth` banner (host 0.9.105): "To sign in, open this
1649
+ * URL in your browser: https://accounts.x.ai/oauth2/device?user_code=XXXX-XXXX
1650
+ * … Confirm this code in your browser: XXXX-XXXX". Phrase gate + code shape,
1651
+ * same posture as the codex parser; grok prints to stderr.
1652
+ */
1653
+ export function findGrokDeviceAuthorization(
1654
+ value: string,
1655
+ ): { verificationUrl: string; userCode: string } | null {
1656
+ const plain = stripTerminalControl(value);
1657
+ if (!/confirm this code|open this url/i.test(plain)) return null;
1658
+ const verificationUrl = findSafeHttpsUrl(plain);
1659
+ if (!verificationUrl) return null;
1660
+ const code =
1661
+ /user_code=([A-Z0-9]{4,8}-[A-Z0-9]{4,10})\b/.exec(plain) ??
1662
+ /(?:^|\s)([A-Z0-9]{4,8}-[A-Z0-9]{4,10})(?:\s|$)/m.exec(plain);
1663
+ if (!code) return null;
1664
+ return { verificationUrl, userCode: code[1]! };
1665
+ }
1666
+
1574
1667
  export function extractClaudeOAuthToken(value: string): string | null {
1575
1668
  const plain = stripTerminalControl(value);
1576
1669
  // The FULL `sk-ant-oat01-` prefix and a realistic minimum length are both
@@ -2003,7 +2096,9 @@ export function engineLoginContainerArgs(
2003
2096
  // because the typescript went to /dev/null.
2004
2097
  `${LOGIN_MOUNT}/typescript`,
2005
2098
  ]
2006
- : // Device-code flow: no localhost callback, no paste — the CLI prints
2099
+ : request.engine === "grok"
2100
+ ? ["sh", "-c", GROK_LOGIN_SHELL]
2101
+ : // Device-code flow: no localhost callback, no paste — the CLI prints
2007
2102
  // a verification URL + one-time code, the operator confirms on the
2008
2103
  // provider's page, and the CLI polls to completion (ADR-116 v2,
2009
2104
  // 2026-08-24). The callback machinery below stays as the fallback
@@ -2050,6 +2145,8 @@ export function engineLoginContainerArgs(
2050
2145
  "--env",
2051
2146
  `CODEX_HOME=${CODEX_HOME}`,
2052
2147
  "--env",
2148
+ `GROK_HOME=${GROK_HOME}`,
2149
+ "--env",
2053
2150
  "ASDF_CONFIG_FILE=/dev/null",
2054
2151
  "--env",
2055
2152
  "ASDF_DEFAULT_TOOL_VERSIONS_FILENAME=.tool-versions",
@@ -2095,6 +2192,8 @@ export function engineLoginContainerArgs(
2095
2192
  "--env",
2096
2193
  `CODEX_HOME=${CODEX_HOME}`,
2097
2194
  "--env",
2195
+ `GROK_HOME=${GROK_HOME}`,
2196
+ "--env",
2098
2197
  "ASDF_CONFIG_FILE=/dev/null",
2099
2198
  "--env",
2100
2199
  "ASDF_DEFAULT_TOOL_VERSIONS_FILENAME=.tool-versions",
@@ -2426,6 +2525,7 @@ export async function createEngineLoginTempDir(
2426
2525
  // self-creates its HOME). Create both up front.
2427
2526
  await mkdir(join(path, "home"), { mode: 0o700 });
2428
2527
  await mkdir(join(path, "codex"), { mode: 0o700 });
2528
+ await mkdir(join(path, "grok"), { mode: 0o700 });
2429
2529
  await syncDirectory(path);
2430
2530
  return path;
2431
2531
  } catch (error) {
@@ -279,10 +279,19 @@ async function hostEgressIp(): Promise<string> {
279
279
  );
280
280
  }
281
281
 
282
+ /** How long a resolved payload AMI is trusted before the newest published
283
+ * image is looked up again. Long enough that a burst of launches shares one
284
+ * describe-images; short enough that a freshly published image reaches every
285
+ * RUNNING host — shared and BYO — on its next task without a host restart
286
+ * (before 0.9.106 the answer was kept for the process lifetime). */
287
+ export const PAYLOAD_AMI_TTL_MS = 5 * 60_000;
288
+
282
289
  export function createAwsMachineProvider(
283
290
  config: AwsMachineConfig,
284
291
  runner: Runner = defaultRunner(config.region),
292
+ deps: { now?: () => number } = {},
285
293
  ): MachineProvider {
294
+ const now = deps.now ?? (() => Date.now());
286
295
  const taskIdOf = (machineId: string): string => {
287
296
  if (!machineId.startsWith("uai-machine-")) {
288
297
  throw new Error(`not a uai machine id: ${machineId}`);
@@ -332,10 +341,12 @@ export function createAwsMachineProvider(
332
341
  // orchestrator can never reach.
333
342
  // -------------------------------------------------------------------------
334
343
 
335
- let resolvedImage: string | null = null;
344
+ let resolvedImage: { id: string; at: number } | null = null;
336
345
  async function resolveImage(specImage: string): Promise<string> {
337
346
  if (specImage.startsWith("ami-")) return specImage;
338
- if (resolvedImage) return resolvedImage;
347
+ if (resolvedImage && now() - resolvedImage.at < PAYLOAD_AMI_TTL_MS) {
348
+ return resolvedImage.id;
349
+ }
339
350
  const res = await runner([
340
351
  "ec2",
341
352
  "describe-images",
@@ -356,12 +367,24 @@ export function createAwsMachineProvider(
356
367
  }
357
368
  }
358
369
  if (typeof image !== "string" || !image.startsWith("ami-")) {
370
+ // A stale-but-known image beats a failed launch when the lookup itself
371
+ // hiccups; only a host that never resolved one has nothing to offer.
372
+ if (resolvedImage) return resolvedImage.id;
359
373
  throw new Error(
360
374
  `no uai machine payload AMI is published in ${config.region} — set ` +
361
375
  "UAI_MACHINE_IMAGE to an AMI id, or ask Uai to publish this region",
362
376
  );
363
377
  }
364
- resolvedImage = image;
378
+ if (resolvedImage?.id !== image) {
379
+ // The journal line that tells an operator which payload a task got —
380
+ // an exit 127 for a missing CLI is otherwise the only symptom of a
381
+ // region that never received the latest publish.
382
+ console.log(
383
+ `[machine-aws] payload image for ${config.region}: ${image}` +
384
+ (resolvedImage ? ` (was ${resolvedImage.id})` : ""),
385
+ );
386
+ }
387
+ resolvedImage = { id: image, at: now() };
365
388
  return image;
366
389
  }
367
390
 
@@ -27,6 +27,7 @@ import {
27
27
  recycleAppleRuntimeServices,
28
28
  } from "./apple-runtime-recycle";
29
29
  import { detectEngine } from "./engines";
30
+ import { hasStoredEngineAccounts } from "./engine-accounts";
30
31
  import {
31
32
  appleContainerRuntimeBinding,
32
33
  claimContainerRuntimeForMaintenance,
@@ -112,11 +113,17 @@ export function configuredOptionalEngines(): {
112
113
  cursor: boolean;
113
114
  opencode: boolean;
114
115
  } {
116
+ // A stored account (a workspace's, on a shared host; a labeled extra on an
117
+ // owned one) needs the CLI in the image just like the host's own sign-in
118
+ // does — without this a grok/cursor account existed but its tasks exited
119
+ // 127 for a missing binary.
120
+ const on = (kind: "kimi" | "grok" | "cursor" | "opencode"): boolean =>
121
+ detectEngine(kind) || hasStoredEngineAccounts(kind);
115
122
  return {
116
- kimi: detectEngine("kimi"),
117
- grok: detectEngine("grok"),
118
- cursor: detectEngine("cursor"),
119
- opencode: detectEngine("opencode"),
123
+ kimi: on("kimi"),
124
+ grok: on("grok"),
125
+ cursor: on("cursor"),
126
+ opencode: on("opencode"),
120
127
  };
121
128
  }
122
129
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.104",
3
+ "version": "0.9.106",
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/main.ts CHANGED
@@ -462,6 +462,9 @@ const engineLoginManager = createEngineLoginManager({
462
462
  getOrchestrator().refreshEngineAccountAgents(engine),
463
463
  tempRoot: join(env.uaiHome, "engine-login-bind"),
464
464
  reconcileOnCreate: false,
465
+ // A grok sign-in may be the first grok configuration on this host: the
466
+ // standard image needs the CLI before its tasks can run (host 0.9.105).
467
+ ensureStandardImage: () => ensureStandardImage(),
465
468
  });
466
469
  // ADR-117: one-shot connect (pasted key / OpenCode re-detect) over the bridge.
467
470
  const engineConnectManager = createEngineConnectManager({
package/src/protocol.ts CHANGED
@@ -326,7 +326,10 @@ export type HostLogsEndFrame =
326
326
  message: string;
327
327
  };
328
328
 
329
- export type EngineLoginKind = "claude" | "codex";
329
+ /** Engines with a REMOTE sign-in (login container driven from the cloud).
330
+ * grok since host 0.9.105: `grok login --device-auth`, same device-code shape
331
+ * as codex. Kimi/OpenCode remain host-terminal (or key) for now. */
332
+ export type EngineLoginKind = "claude" | "codex" | "grok";
330
333
  export type EngineLoginPhase =
331
334
  | "starting"
332
335
  | "authorize"
@@ -668,7 +671,7 @@ function isBoundedMessage(value: unknown): value is string {
668
671
  }
669
672
 
670
673
  function isEngineLoginKind(value: unknown): value is EngineLoginKind {
671
- return value === "claude" || value === "codex";
674
+ return value === "claude" || value === "codex" || value === "grok";
672
675
  }
673
676
 
674
677
  function isEngineLoginErrorCode(value: unknown): value is EngineLoginErrorCode {