@runuai/host 0.9.108 → 0.9.110

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.
@@ -183,7 +183,17 @@ export class GrokSession implements AgentSession {
183
183
  argv: argv as [string, ...string[]],
184
184
  user: "node",
185
185
  inheritEnv,
186
- env: this.agentEnv,
186
+ env: {
187
+ ...this.agentEnv,
188
+ // Grok gates MCP/LSP servers (even user-scope ones) behind folder
189
+ // trust, and /workspace is never trusted: it is not a git repo and
190
+ // headless runs have no prompt. `grok mcp doctor` on a task machine:
191
+ // "folder untrusted … not started for an untrusted folder"; with this
192
+ // off, "2 healthy". The container/machine IS the isolation boundary —
193
+ // the same reasoning as --permission-mode bypassPermissions above
194
+ // (live 2026-09-08, first Grok agents with MCP servers).
195
+ GROK_FOLDER_TRUST: "0",
196
+ },
187
197
  maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
188
198
  };
189
199
  }
@@ -243,14 +243,24 @@ const SERVER_DEF = { command: SERVER_COMMAND, args: SERVER_ARGS };
243
243
  * `--approve-mcps`); same JSON shape as Claude's project file. */
244
244
  const CURSOR_MCP_PATH = "/home/node/.cursor/mcp.json";
245
245
 
246
- /** Grok: `grok mcp add` (add-or-update) in PROJECT scope writes
247
- * /workspace/.grok/config.toml, honoured by every GROK_HOME and hot-reloaded,
248
- * so per-account homes need no per-home write. The launcher has no single
249
- * quotes by construction (see SERVER_LAUNCHER), so it survives the quoting. */
246
+ /** Grok: `grok mcp add` (add-or-update) in USER scope, once per GROK_HOME a
247
+ * task may run under (the default ~/.grok and each ~/.grok-acct-<id>) —
248
+ * `GROK_HOME` redirects the user config. PROJECT scope looked simpler but
249
+ * grok refuses repo-local servers in an untrusted folder, and /workspace is
250
+ * not a git repo so nothing ever trusts it (live 2026-09-08: "folder
251
+ * untrusted … re-run with --trust", no such flag headless). The launcher has
252
+ * no single quotes by construction (see SERVER_LAUNCHER). */
250
253
  const GROK_BIN = "/home/node/.grok/bin/grok";
251
- export const GROK_BROWSER_ADD_CMD =
252
- `cd /workspace && ${GROK_BIN} mcp add browser -s project -t stdio ` +
253
- `${SERVER_COMMAND} -- ${SERVER_ARGS.map((arg) => `'${arg}'`).join(" ")}`;
254
+ export const DEFAULT_GROK_HOME = "/home/node/.grok";
255
+ export function grokBrowserAddCommand(home: string): string {
256
+ if (!/^\/home\/node\/\.grok(-acct-[A-Za-z0-9_-]+)?$/.test(home)) {
257
+ throw new Error("invalid Grok home");
258
+ }
259
+ return (
260
+ `GROK_HOME=${home} ${GROK_BIN} mcp add browser -s user -t stdio ` +
261
+ `${SERVER_COMMAND} -- ${SERVER_ARGS.map((arg) => `'${arg}'`).join(" ")}`
262
+ );
263
+ }
254
264
 
255
265
  const CLAUDE_SETTINGS_JSON = JSON.stringify(
256
266
  { enableAllProjectMcpServers: true },
@@ -1081,9 +1091,14 @@ export async function setupBrowserTesting(
1081
1091
  * (2026-09-08 — until then only Claude/Codex were wired and both answered
1082
1092
  * "no browser MCP in this session" on the Studio). */
1083
1093
  engineKinds: readonly string[] = [],
1094
+ /** Every GROK_HOME the task may run under (default + account homes). */
1095
+ grokHomes: readonly string[] = [DEFAULT_GROK_HOME],
1084
1096
  ): Promise<BrowserSetup> {
1085
1097
  const hasCursor = engineKinds.includes("cursor");
1086
1098
  const hasGrok = engineKinds.includes("grok");
1099
+ const selectedGrokHomes = hasGrok
1100
+ ? [...new Set(grokHomes.length > 0 ? grokHomes : [DEFAULT_GROK_HOME])]
1101
+ : [];
1087
1102
  // Machine sessions run under MACHINE_RUNTIME_AUTHORITY_ENV, so every setup
1088
1103
  // exec here must too — the prewarm in particular. Warming npx under the
1089
1104
  // CONTAINER authority (private NPM_CONFIG_CACHE) leaves the session-default
@@ -1359,7 +1374,11 @@ export async function setupBrowserTesting(
1359
1374
  // GROK_BROWSER_ADD_CMD) rather than a file we own; a failure here leaves
1360
1375
  // the task configured-false so the next ensure retries.
1361
1376
  if (hasGrok) {
1362
- const grokArgv = ["sh", "-lc", GROK_BROWSER_ADD_CMD] as [string, ...string[]];
1377
+ const grokArgv = [
1378
+ "sh",
1379
+ "-lc",
1380
+ selectedGrokHomes.map(grokBrowserAddCommand).join(" && "),
1381
+ ] as [string, ...string[]];
1363
1382
  const registered = environment
1364
1383
  ? await environment.exec({
1365
1384
  argv: grokArgv,
@@ -1056,8 +1056,8 @@ const MCP_CONFIG_HOST_TIMEOUT_MS = 40_000;
1056
1056
  * - Codex — mcp-remote shims appended once per slug to config.toml.
1057
1057
  * - Cursor — ~/.cursor/mcp.json ({ url } form; the adapter passes
1058
1058
  * `--approve-mcps`).
1059
- * - Grok — `grok mcp add -s project` writes /workspace/.grok/config.toml
1060
- * (idempotent per slug; applies to every GROK_HOME).
1059
+ * - Grok — `grok mcp add -s user` into every GROK_HOME the task may use
1060
+ * (idempotent per slug; project scope is refused in an untrusted folder).
1061
1061
  * - OpenCode — ~/.config/opencode/opencode.json `mcp` block (type:"remote",
1062
1062
  * oauth:false so it treats the gateway URL as a plain server; the path
1063
1063
  * token is the auth). Written only when the task has connections.
@@ -1071,6 +1071,8 @@ export async function setupMcpTaskConfig(
1071
1071
  codexHomes: readonly string[] = ["/home/node/.codex"],
1072
1072
  environment?: Pick<TaskEnvironmentHandle, "exec"> &
1073
1073
  Partial<Pick<TaskEnvironmentHandle, "descriptor">>,
1074
+ /** Every GROK_HOME the task may run under (default + account homes). */
1075
+ grokHomes: readonly string[] = ["/home/node/.grok"],
1074
1076
  ): Promise<boolean> {
1075
1077
  // No early return on empty: the claude adapter passes
1076
1078
  // `--mcp-config /workspace/.mcp.json` unconditionally (ADR-057), so the
@@ -1153,18 +1155,22 @@ export async function setupMcpTaskConfig(
1153
1155
  )}`,
1154
1156
  ]
1155
1157
  : []),
1156
- // Grok: PROJECT-scope `grok mcp add` writes /workspace/.grok/config.toml,
1157
- // which every GROK_HOME (the default or a per-account dir) picks up —
1158
- // grok hot-reloads repo config. The binary lives at ~/.grok/bin (x.ai
1159
- // installer); the old ~/.local/bin path failed silently behind `|| true`
1160
- // and grok never saw a single MCP server (live 2026-09-08).
1158
+ // Grok: USER-scope `grok mcp add` once per GROK_HOME the task may run
1159
+ // under (GROK_HOME redirects the user config). Project scope is refused
1160
+ // for an untrusted folder and /workspace is no git repo (live
1161
+ // 2026-09-08). The binary lives at ~/.grok/bin (x.ai installer); the
1162
+ // old ~/.local/bin path failed silently behind `|| true`.
1161
1163
  ...(has("grok")
1162
- ? connections.map(
1163
- (c) =>
1164
- `(cd /workspace && ${GROK_BIN} mcp add ${shellQuote(c.slug)} ${shellQuote(
1165
- urlFor(c.slug),
1166
- )} -t http -s project >/dev/null 2>&1) || true`,
1167
- )
1164
+ ? grokHomes
1165
+ .filter((home) => /^\/home\/node\/\.grok(-acct-[A-Za-z0-9_-]+)?$/.test(home))
1166
+ .flatMap((home) =>
1167
+ connections.map(
1168
+ (c) =>
1169
+ `GROK_HOME=${home} ${GROK_BIN} mcp add ${shellQuote(c.slug)} ${shellQuote(
1170
+ urlFor(c.slug),
1171
+ )} -t http -s user >/dev/null 2>&1 || true`,
1172
+ ),
1173
+ )
1168
1174
  : []),
1169
1175
  // OpenCode: global config `mcp` block (auto-discovered regardless of the
1170
1176
  // exec cwd, and independent of any per-account OPENCODE_DATA_DIR).
@@ -101,6 +101,7 @@ import {
101
101
  } from "./agent-cli";
102
102
  import {
103
103
  DEFAULT_CODEX_HOME,
104
+ DEFAULT_GROK_HOME,
104
105
  setupBrowserTesting,
105
106
  type BrowserSetup,
106
107
  } from "./browser-testing";
@@ -814,6 +815,16 @@ export class Orchestrator {
814
815
  return [...new Set([DEFAULT_CODEX_HOME, ...homes])];
815
816
  }
816
817
 
818
+ /** Every GROK_HOME a task in `scope` may run under: the default plus each
819
+ * sign-in account's isolated home (API-key accounts use the default). */
820
+ private grokHomes(hasGrok: boolean, scope: string): string[] {
821
+ if (!hasGrok) return [];
822
+ const homes = resolveEngineAccounts("grok", {}, scope)
823
+ .map((account) => account.configDir?.containerDir)
824
+ .filter((home): home is string => Boolean(home));
825
+ return [...new Set([DEFAULT_GROK_HOME, ...homes])];
826
+ }
827
+
817
828
  private noteProvisionMutation(
818
829
  channel: Channel,
819
830
  waitForBrowserReadiness: boolean,
@@ -1026,6 +1037,7 @@ export class Orchestrator {
1026
1037
  requestedHomes,
1027
1038
  channel.agentHandle,
1028
1039
  requestedKinds,
1040
+ this.grokHomes(requestedKinds.includes("grok"), channel.engineAccountScope),
1029
1041
  )
1030
1042
  : await setupBrowserTesting(
1031
1043
  channel.taskId,
@@ -1034,6 +1046,7 @@ export class Orchestrator {
1034
1046
  requestedHomes,
1035
1047
  undefined,
1036
1048
  requestedKinds,
1049
+ this.grokHomes(requestedKinds.includes("grok"), channel.engineAccountScope),
1037
1050
  );
1038
1051
  } catch (err) {
1039
1052
  console.warn(
@@ -1190,10 +1203,15 @@ export class Orchestrator {
1190
1203
  const codexHomes = engineKinds.includes("codex")
1191
1204
  ? this.codexHomes(true, channel.engineAccountScope)
1192
1205
  : [];
1206
+ const grokHomes = this.grokHomes(
1207
+ engineKinds.includes("grok"),
1208
+ channel.engineAccountScope,
1209
+ );
1193
1210
  const fingerprint = JSON.stringify({
1194
1211
  connections,
1195
1212
  engineKinds,
1196
1213
  codexHomes,
1214
+ ...(grokHomes.length > 0 ? { grokHomes } : {}),
1197
1215
  });
1198
1216
  const dirtyEpoch = channel.mcpConfigDirtyEpoch;
1199
1217
  const repairingProvisionedConfig =
@@ -1240,6 +1258,7 @@ export class Orchestrator {
1240
1258
  engineKinds,
1241
1259
  codexHomes,
1242
1260
  providerEnvironment,
1261
+ grokHomes,
1243
1262
  )
1244
1263
  : await setupMcpTaskConfig(
1245
1264
  channel.taskId,
@@ -1247,6 +1266,8 @@ export class Orchestrator {
1247
1266
  connections,
1248
1267
  engineKinds,
1249
1268
  codexHomes,
1269
+ undefined,
1270
+ grokHomes,
1250
1271
  );
1251
1272
  if (!configured || !this.isActiveChannel(channel)) return;
1252
1273
  channel.configGeneration += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.108",
3
+ "version": "0.9.110",
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>",