@runuai/host 0.9.106 → 0.9.108
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/browser-testing.ts +98 -1
- package/lib/mcp-gateway.ts +11 -4
- package/lib/orchestrator.ts +3 -0
- package/package.json +1 -1
- package/src/index.ts +11 -0
- package/src/main.ts +14 -0
- package/src/protocol.ts +10 -0
package/lib/browser-testing.ts
CHANGED
|
@@ -236,9 +236,22 @@ const SERVER_COMMAND = "sh";
|
|
|
236
236
|
/** Exported for the test that locks the pin/browser invariants. */
|
|
237
237
|
export const SERVER_ARGS: [string, string] = ["-lc", SERVER_LAUNCHER];
|
|
238
238
|
|
|
239
|
-
/** The `browser` server definition
|
|
239
|
+
/** The `browser` server definition every engine gets. */
|
|
240
240
|
const SERVER_DEF = { command: SERVER_COMMAND, args: SERVER_ARGS };
|
|
241
241
|
|
|
242
|
+
/** Cursor reads stdio servers from ~/.cursor/mcp.json (the adapter passes
|
|
243
|
+
* `--approve-mcps`); same JSON shape as Claude's project file. */
|
|
244
|
+
const CURSOR_MCP_PATH = "/home/node/.cursor/mcp.json";
|
|
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. */
|
|
250
|
+
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
|
+
|
|
242
255
|
const CLAUDE_SETTINGS_JSON = JSON.stringify(
|
|
243
256
|
{ enableAllProjectMcpServers: true },
|
|
244
257
|
null,
|
|
@@ -562,6 +575,45 @@ function ensureClaudeBrowser() {
|
|
|
562
575
|
return { configured: true, changed: true };
|
|
563
576
|
}
|
|
564
577
|
|
|
578
|
+
function ensureCursorBrowser() {
|
|
579
|
+
const file = process.env.UAI_CURSOR_MCP_PATH;
|
|
580
|
+
if (!file) return { configured: true, changed: false };
|
|
581
|
+
const exists = fs.existsSync(file);
|
|
582
|
+
let original = null;
|
|
583
|
+
let document;
|
|
584
|
+
try {
|
|
585
|
+
original = exists ? fs.readFileSync(file, "utf8") : null;
|
|
586
|
+
document = exists ? JSON.parse(original) : { mcpServers: {} };
|
|
587
|
+
} catch {
|
|
588
|
+
notes.push("cursor mcp.json is unparseable - left alone");
|
|
589
|
+
return { configured: false, changed: false };
|
|
590
|
+
}
|
|
591
|
+
if (!plainObject(document)) {
|
|
592
|
+
notes.push("cursor mcp.json root is not an object - left alone");
|
|
593
|
+
return { configured: false, changed: false };
|
|
594
|
+
}
|
|
595
|
+
if (document.mcpServers === undefined) document.mcpServers = {};
|
|
596
|
+
if (!plainObject(document.mcpServers)) {
|
|
597
|
+
notes.push("cursor mcp.json mcpServers is not an object - left alone");
|
|
598
|
+
return { configured: false, changed: false };
|
|
599
|
+
}
|
|
600
|
+
const current = document.mcpServers.browser;
|
|
601
|
+
if (current !== undefined && equal(current, def)) {
|
|
602
|
+
return { configured: true, changed: false };
|
|
603
|
+
}
|
|
604
|
+
if (current !== undefined && !isManaged(current)) {
|
|
605
|
+
notes.push("cursor mcp.json browser entry is not uai-managed - left alone");
|
|
606
|
+
return { configured: true, changed: false };
|
|
607
|
+
}
|
|
608
|
+
document.mcpServers.browser = def;
|
|
609
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
610
|
+
if (!atomicWrite(file, JSON.stringify(document, null, 2) + "\n", original)) {
|
|
611
|
+
notes.push("cursor mcp.json changed concurrently - retrying later");
|
|
612
|
+
return { configured: false, changed: false };
|
|
613
|
+
}
|
|
614
|
+
return { configured: true, changed: true };
|
|
615
|
+
}
|
|
616
|
+
|
|
565
617
|
function ensureClaudeSettings() {
|
|
566
618
|
const file = process.env.UAI_CLAUDE_SETTINGS_PATH;
|
|
567
619
|
if (!file || fs.existsSync(file)) return { configured: true, changed: false };
|
|
@@ -913,6 +965,9 @@ function safeOutcome(label, ensure) {
|
|
|
913
965
|
for (const outcome of [
|
|
914
966
|
safeOutcome("Claude browser config", ensureClaudeBrowser),
|
|
915
967
|
safeOutcome("Claude settings", ensureClaudeSettings),
|
|
968
|
+
...(process.env.UAI_CURSOR_MCP_PATH
|
|
969
|
+
? [safeOutcome("Cursor browser config", ensureCursorBrowser)]
|
|
970
|
+
: []),
|
|
916
971
|
...codexHomes.map(function (home) {
|
|
917
972
|
return safeOutcome(home, function () { return ensureCodexBrowser(home); });
|
|
918
973
|
}),
|
|
@@ -1022,7 +1077,13 @@ export async function setupBrowserTesting(
|
|
|
1022
1077
|
codexHomes: readonly string[] = hasCodex ? [DEFAULT_CODEX_HOME] : [],
|
|
1023
1078
|
environment?: Pick<TaskEnvironmentHandle, "exec" | "launchDetachedSession"> &
|
|
1024
1079
|
Partial<Pick<TaskEnvironmentHandle, "descriptor">>,
|
|
1080
|
+
/** The roster's agent kinds: cursor and grok get the browser server too
|
|
1081
|
+
* (2026-09-08 — until then only Claude/Codex were wired and both answered
|
|
1082
|
+
* "no browser MCP in this session" on the Studio). */
|
|
1083
|
+
engineKinds: readonly string[] = [],
|
|
1025
1084
|
): Promise<BrowserSetup> {
|
|
1085
|
+
const hasCursor = engineKinds.includes("cursor");
|
|
1086
|
+
const hasGrok = engineKinds.includes("grok");
|
|
1026
1087
|
// Machine sessions run under MACHINE_RUNTIME_AUTHORITY_ENV, so every setup
|
|
1027
1088
|
// exec here must too — the prewarm in particular. Warming npx under the
|
|
1028
1089
|
// CONTAINER authority (private NPM_CONFIG_CACHE) leaves the session-default
|
|
@@ -1201,6 +1262,7 @@ export async function setupBrowserTesting(
|
|
|
1201
1262
|
...(selectedCodexHomes.length > 0
|
|
1202
1263
|
? { UAI_CODEX_HOMES: JSON.stringify(selectedCodexHomes) }
|
|
1203
1264
|
: {}),
|
|
1265
|
+
...(hasCursor ? { UAI_CURSOR_MCP_PATH: CURSOR_MCP_PATH } : {}),
|
|
1204
1266
|
};
|
|
1205
1267
|
const writeArgv = [
|
|
1206
1268
|
"flock",
|
|
@@ -1238,6 +1300,7 @@ export async function setupBrowserTesting(
|
|
|
1238
1300
|
...(selectedCodexHomes.length > 0
|
|
1239
1301
|
? ["-e", `UAI_CODEX_HOMES=${JSON.stringify(selectedCodexHomes)}`]
|
|
1240
1302
|
: []),
|
|
1303
|
+
...(hasCursor ? ["-e", `UAI_CURSOR_MCP_PATH=${CURSOR_MCP_PATH}`] : []),
|
|
1241
1304
|
...runtimeAuthorityDockerExecArgs(),
|
|
1242
1305
|
container,
|
|
1243
1306
|
...writeArgv,
|
|
@@ -1292,6 +1355,40 @@ export async function setupBrowserTesting(
|
|
|
1292
1355
|
}
|
|
1293
1356
|
}
|
|
1294
1357
|
|
|
1358
|
+
// Grok registers its servers through its own CLI (project scope, see
|
|
1359
|
+
// GROK_BROWSER_ADD_CMD) rather than a file we own; a failure here leaves
|
|
1360
|
+
// the task configured-false so the next ensure retries.
|
|
1361
|
+
if (hasGrok) {
|
|
1362
|
+
const grokArgv = ["sh", "-lc", GROK_BROWSER_ADD_CMD] as [string, ...string[]];
|
|
1363
|
+
const registered = environment
|
|
1364
|
+
? await environment.exec({
|
|
1365
|
+
argv: grokArgv,
|
|
1366
|
+
cwd: "/workspace",
|
|
1367
|
+
env: authorityEnv,
|
|
1368
|
+
timeoutMs: 30_000,
|
|
1369
|
+
maxOutputBytes: 64 * 1024,
|
|
1370
|
+
})
|
|
1371
|
+
: await taskContainerCli(
|
|
1372
|
+
[
|
|
1373
|
+
"exec",
|
|
1374
|
+
"-w",
|
|
1375
|
+
"/workspace",
|
|
1376
|
+
...runtimeAuthorityDockerExecArgs(),
|
|
1377
|
+
container,
|
|
1378
|
+
...grokArgv,
|
|
1379
|
+
],
|
|
1380
|
+
{ timeoutMs: 30_000 },
|
|
1381
|
+
);
|
|
1382
|
+
const grokStatus =
|
|
1383
|
+
"exitCode" in registered ? registered.exitCode : registered.status;
|
|
1384
|
+
if (grokStatus !== 0) {
|
|
1385
|
+
configured = false;
|
|
1386
|
+
console.warn(
|
|
1387
|
+
`[browser] task ${taskId}: grok browser MCP registration failed (exit ${String(grokStatus)})`,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1295
1392
|
// Chromium apt libs + the viewer stack packages, then start the stack.
|
|
1296
1393
|
// The lock is a container-local flock acquired BEFORE detach. The worker
|
|
1297
1394
|
// inherits its open file description, so failures and even SIGKILL release
|
package/lib/mcp-gateway.ts
CHANGED
|
@@ -1042,6 +1042,8 @@ function shellQuote(value: string): string {
|
|
|
1042
1042
|
// client does not kill its in-container child. The full writer (including its
|
|
1043
1043
|
// 10s lock wait and every engine-specific command) gets 25s before TERM and
|
|
1044
1044
|
// 5s more before KILL, below the 40s host backstop.
|
|
1045
|
+
/** The grok CLI inside task containers/machines (x.ai installer path). */
|
|
1046
|
+
const GROK_BIN = "/home/node/.grok/bin/grok";
|
|
1045
1047
|
const MCP_CONFIG_WRITE_TIMEOUT_S = 25;
|
|
1046
1048
|
const MCP_CONFIG_WRITE_KILL_AFTER_S = 5;
|
|
1047
1049
|
const MCP_CONFIG_HOST_TIMEOUT_MS = 40_000;
|
|
@@ -1054,7 +1056,8 @@ const MCP_CONFIG_HOST_TIMEOUT_MS = 40_000;
|
|
|
1054
1056
|
* - Codex — mcp-remote shims appended once per slug to config.toml.
|
|
1055
1057
|
* - Cursor — ~/.cursor/mcp.json ({ url } form; the adapter passes
|
|
1056
1058
|
* `--approve-mcps`).
|
|
1057
|
-
* - Grok — `grok mcp add` writes
|
|
1059
|
+
* - Grok — `grok mcp add -s project` writes /workspace/.grok/config.toml
|
|
1060
|
+
* (idempotent per slug; applies to every GROK_HOME).
|
|
1058
1061
|
* - OpenCode — ~/.config/opencode/opencode.json `mcp` block (type:"remote",
|
|
1059
1062
|
* oauth:false so it treats the gateway URL as a plain server; the path
|
|
1060
1063
|
* token is the auth). Written only when the task has connections.
|
|
@@ -1150,13 +1153,17 @@ export async function setupMcpTaskConfig(
|
|
|
1150
1153
|
)}`,
|
|
1151
1154
|
]
|
|
1152
1155
|
: []),
|
|
1153
|
-
// Grok: `grok mcp add` writes
|
|
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).
|
|
1154
1161
|
...(has("grok")
|
|
1155
1162
|
? connections.map(
|
|
1156
1163
|
(c) =>
|
|
1157
|
-
|
|
1164
|
+
`(cd /workspace && ${GROK_BIN} mcp add ${shellQuote(c.slug)} ${shellQuote(
|
|
1158
1165
|
urlFor(c.slug),
|
|
1159
|
-
)} -t http -s
|
|
1166
|
+
)} -t http -s project >/dev/null 2>&1) || true`,
|
|
1160
1167
|
)
|
|
1161
1168
|
: []),
|
|
1162
1169
|
// OpenCode: global config `mcp` block (auto-discovered regardless of the
|
package/lib/orchestrator.ts
CHANGED
|
@@ -1025,12 +1025,15 @@ export class Orchestrator {
|
|
|
1025
1025
|
requestedHasCodex,
|
|
1026
1026
|
requestedHomes,
|
|
1027
1027
|
channel.agentHandle,
|
|
1028
|
+
requestedKinds,
|
|
1028
1029
|
)
|
|
1029
1030
|
: await setupBrowserTesting(
|
|
1030
1031
|
channel.taskId,
|
|
1031
1032
|
channel.containerName,
|
|
1032
1033
|
requestedHasCodex,
|
|
1033
1034
|
requestedHomes,
|
|
1035
|
+
undefined,
|
|
1036
|
+
requestedKinds,
|
|
1034
1037
|
);
|
|
1035
1038
|
} catch (err) {
|
|
1036
1039
|
console.warn(
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -123,6 +123,7 @@ import {
|
|
|
123
123
|
renameEngineAccount,
|
|
124
124
|
reorderEngineAccounts,
|
|
125
125
|
} from "../lib/engine-accounts";
|
|
126
|
+
import { disconnectEngine } from "../lib/engines";
|
|
126
127
|
|
|
127
128
|
export {
|
|
128
129
|
parseMentions,
|
|
@@ -1271,6 +1272,16 @@ export const hostCommands: HostCommands = {
|
|
|
1271
1272
|
}
|
|
1272
1273
|
},
|
|
1273
1274
|
|
|
1275
|
+
async engineDisconnect(ctx, input) {
|
|
1276
|
+
logCommand(ctx, "engineDisconnect", input.engine);
|
|
1277
|
+
try {
|
|
1278
|
+
disconnectEngine(input.engine);
|
|
1279
|
+
return ok({ disconnected: true });
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
return failFromUnknown(err);
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
|
|
1274
1285
|
async channelResolvePermission(ctx, taskId, agentId, requestId, decision) {
|
|
1275
1286
|
logCommand(ctx, "channelResolvePermission", taskId, agentId, requestId);
|
|
1276
1287
|
try {
|
package/src/main.ts
CHANGED
|
@@ -188,6 +188,7 @@ import {
|
|
|
188
188
|
HOST_ENGINE_CONNECT_PROTOCOL_FEATURE,
|
|
189
189
|
HOST_ENGINE_ACCOUNT_MANAGE_PROTOCOL_FEATURE,
|
|
190
190
|
HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE,
|
|
191
|
+
HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
|
|
191
192
|
HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
|
|
192
193
|
isEngineAccountScope,
|
|
193
194
|
HOST_LOGS_PROTOCOL_FEATURE,
|
|
@@ -232,6 +233,7 @@ import {
|
|
|
232
233
|
type FilesOpInput,
|
|
233
234
|
engineAccountScopeFromWire,
|
|
234
235
|
isEngineAccountLabel,
|
|
236
|
+
isEngineConnectKind,
|
|
235
237
|
} from "./protocol";
|
|
236
238
|
|
|
237
239
|
setContainerRuntimeOperationalCheck(() => containerRuntimeProblem() === null);
|
|
@@ -615,6 +617,7 @@ function buildCapabilities(): HostCapabilities {
|
|
|
615
617
|
HOST_ENGINE_CONNECT_PROTOCOL_FEATURE,
|
|
616
618
|
HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE,
|
|
617
619
|
HOST_ENGINE_ACCOUNT_MANAGE_PROTOCOL_FEATURE,
|
|
620
|
+
HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
|
|
618
621
|
// The echo adapter cannot execute the in-task CLI. Advertising typed
|
|
619
622
|
// dispatch in mock mode would let the composer create a Secretary that
|
|
620
623
|
// has no way to wake crew.
|
|
@@ -2465,6 +2468,16 @@ async function dispatchCommand(
|
|
|
2465
2468
|
if (reordered.ok) sendCapabilities();
|
|
2466
2469
|
return reordered;
|
|
2467
2470
|
}
|
|
2471
|
+
case "engineDisconnect": {
|
|
2472
|
+
const value = args[0];
|
|
2473
|
+
const engine = (value as { engine?: unknown } | undefined)?.engine;
|
|
2474
|
+
if (!isEngineConnectKind(engine)) {
|
|
2475
|
+
throw new Error("invalid command args: expected an engine kind at 0");
|
|
2476
|
+
}
|
|
2477
|
+
const result = await hostCommands.engineDisconnect(ctx, { engine });
|
|
2478
|
+
if (result.ok) sendCapabilities();
|
|
2479
|
+
return result;
|
|
2480
|
+
}
|
|
2468
2481
|
}
|
|
2469
2482
|
}
|
|
2470
2483
|
|
|
@@ -3050,6 +3063,7 @@ const HOST_COMMAND_NAMES = {
|
|
|
3050
3063
|
engineAccountRemove: true,
|
|
3051
3064
|
engineAccountRename: true,
|
|
3052
3065
|
engineAccountReorder: true,
|
|
3066
|
+
engineDisconnect: true,
|
|
3053
3067
|
} satisfies Record<keyof HostCommands, true>;
|
|
3054
3068
|
|
|
3055
3069
|
function isHostCommand(command: string): command is keyof HostCommands {
|
package/src/protocol.ts
CHANGED
|
@@ -113,6 +113,9 @@ export const HOST_ENGINE_CONNECT_PROTOCOL_FEATURE = "host-engine-connect-v1";
|
|
|
113
113
|
* `engineAccountReorder` commands; login `succeeded` events carry the created
|
|
114
114
|
* `accountId` for labeled captures; codex `awaiting_confirmation` carries the
|
|
115
115
|
* device `code`. Extras are advertised in priority order (position). */
|
|
116
|
+
/** Host 0.9.107: `engineDisconnect` — forget the HOST's own credential for an
|
|
117
|
+
* engine (the default slot: .env.local key / login file). Owner-only. */
|
|
118
|
+
export const HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE = "host-engine-disconnect-v1";
|
|
116
119
|
export const HOST_ENGINE_ACCOUNT_MANAGE_PROTOCOL_FEATURE =
|
|
117
120
|
"host-engine-account-manage-v1";
|
|
118
121
|
export const HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE =
|
|
@@ -1493,6 +1496,13 @@ export interface HostCommands {
|
|
|
1493
1496
|
ctx: CommandContext,
|
|
1494
1497
|
input: { kind: string; ids: string[]; scope?: string },
|
|
1495
1498
|
): Promise<HostCommandResult<{ reordered: boolean; message?: string }>>;
|
|
1499
|
+
/** Disconnect-v1: forget the host's DEFAULT credential for an engine — the
|
|
1500
|
+
* .env.local key and/or the login file the adapter detects. Extras are
|
|
1501
|
+
* untouched (remove them by id). Re-advertises on success. */
|
|
1502
|
+
engineDisconnect(
|
|
1503
|
+
ctx: CommandContext,
|
|
1504
|
+
input: { engine: EngineConnectKind },
|
|
1505
|
+
): Promise<HostCommandResult<{ disconnected: boolean; message?: string }>>;
|
|
1496
1506
|
cloneRepo(
|
|
1497
1507
|
ctx: CommandContext,
|
|
1498
1508
|
input: CloneRepoInput,
|