@runuai/host 0.9.104 → 0.9.105
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/engine-accounts.ts +38 -6
- package/lib/engine-login.ts +122 -22
- package/lib/standard-image.ts +11 -4
- package/package.json +1 -1
- package/src/main.ts +3 -0
- package/src/protocol.ts +5 -2
package/lib/engine-accounts.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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(
|
|
924
|
+
position: nextPosition(kind, scope),
|
|
893
925
|
})
|
|
894
926
|
.run();
|
|
895
|
-
return { ok: true, message: `Added
|
|
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
|
package/lib/engine-login.ts
CHANGED
|
@@ -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 =
|
|
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]
|
|
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 (
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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:
|
|
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.
|
|
1081
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
:
|
|
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) {
|
package/lib/standard-image.ts
CHANGED
|
@@ -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:
|
|
117
|
-
grok:
|
|
118
|
-
cursor:
|
|
119
|
-
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
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
|
-
|
|
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 {
|