Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions agent-node/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3590,8 +3590,10 @@ async function ensureGrokCopresenceRuntime(): Promise<GrokCopresenceSession> {
const {
prepareGrokCliHome,
assertNoDiscoveredGrokHooks,
assertGrokCommhubMcpDoctor,
grokCliStateKey,
grokProjectPolicyPaths,
resolveGrokCommhubMcpCommand,
} = await import("./runtime/grok-build-cli-home");
const {
assertGrokCopresenceFeatures,
Expand Down Expand Up @@ -3623,6 +3625,10 @@ async function ensureGrokCopresenceRuntime(): Promise<GrokCopresenceSession> {
if (leaderSocket === attachSocket) {
throw new Error("grok leader and attach sockets must use different paths");
}
const commhubMcpCommand = resolveGrokCommhubMcpCommand(
process.env.BUN_BIN || "bun",
process.env.PATH || "",
);

const prepareRuntime = () => {
const grokCliHome = prepareGrokCliHome({
Expand All @@ -3632,6 +3638,7 @@ async function ensureGrokCopresenceRuntime(): Promise<GrokCopresenceSession> {
projectCwd: grokCwd,
useLeader: true,
commhubMcp: {
command: commhubMcpCommand,
serverPath: commhubMcpServer,
envFile: commhubMcpEnv,
alias: currentAlias(),
Expand Down Expand Up @@ -3725,6 +3732,22 @@ async function ensureGrokCopresenceRuntime(): Promise<GrokCopresenceSession> {
}
assertNoDiscoveredGrokHooks(inspection);
assertGrokCopresenceApprovalOwnership(inspection, auditRuntime.grokCliHome.home);
let doctor: string;
try {
doctor = execFileSync(grokBinary, ["mcp", "doctor", "commhub", "--json"], {
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
timeout: 10_000,
maxBuffer: 2 * 1024 * 1024,
cwd: grokCwd,
env: auditRuntime.env,
});
} catch (error: any) {
throw new Error(
`grok copresence CommHub MCP readiness preflight failed (${error?.code || error?.status || "doctor error"})`,
);
}
assertGrokCommhubMcpDoctor(doctor);
// Clear runtime-owned executable state once more after the inspector.
return prepareRuntime();
};
Expand Down
61 changes: 59 additions & 2 deletions agent-node/src/runtime/grok-build-cli-home.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
mkdirSync,
readdirSync,
readFileSync,
realpathSync,
rmSync,
statSync,
symlinkSync,
Expand All @@ -18,12 +19,14 @@ import { homedir } from "os";
import { basename, dirname, join, resolve } from "path";
import {
acquireGrokProjectTurnLock,
assertGrokCommhubMcpDoctor,
assertNoDiscoveredGrokHooks,
cleanupGrokCliPostStopState,
cleanupGrokCliStoppedTuiGeneration,
grokCliStateKey,
GROK_POST_STOP_CLEANUP_POLICY,
prepareGrokCliHome,
resolveGrokCommhubMcpCommand,
} from "./grok-build-cli-home";
import { assertGrokCopresenceAgentProfile } from "./grok-copresence/policy";

Expand All @@ -35,6 +38,53 @@ afterEach(() => {
});

describe("prepareGrokCliHome", () => {
it("resolves the CommHub MCP command to one canonical executable", () => {
const root = mkdtempSync(join(tmpdir(), "grok-cli-mcp-command-"));
roots.push(root);
const bin = join(root, "bin");
mkdirSync(bin);
const bun = join(bin, "bun");
writeFileSync(bun, "#!/bin/sh\nexit 0\n", { mode: 0o700 });

expect(resolveGrokCommhubMcpCommand("bun", bin)).toBe(realpathSync(bun));
expect(resolveGrokCommhubMcpCommand(bun, "/missing")).toBe(realpathSync(bun));
chmodSync(bun, 0o600);
expect(() => resolveGrokCommhubMcpCommand("bun", bin))
.toThrow("could not be resolved or executed");
expect(() => resolveGrokCommhubMcpCommand("bun", "/missing"))
.toThrow("could not be resolved or executed");
});

it("requires a real CommHub MCP doctor handshake and all three tools", () => {
const healthy = JSON.stringify({
healthy_count: 1,
failing_count: 0,
servers: [{
name: "commhub",
healthy: true,
checks: [
{ label: "command found", passed: true },
{ label: "server started", passed: true },
{ label: "handshake OK", passed: true },
{ label: "4 tools discovered", passed: true },
],
}],
});
expect(() => assertGrokCommhubMcpDoctor(healthy)).not.toThrow();
const missingCommand = JSON.parse(healthy);
missingCommand.healthy_count = 0;
missingCommand.failing_count = 1;
missingCommand.servers[0].healthy = false;
missingCommand.servers[0].checks[0].passed = false;
expect(() => assertGrokCommhubMcpDoctor(JSON.stringify(missingCommand)))
.toThrow("command found");
const missingTools = JSON.parse(healthy);
missingTools.servers[0].checks.pop();
expect(() => assertGrokCommhubMcpDoctor(JSON.stringify(missingTools)))
.toThrow("4 tools discovered");
expect(() => assertGrokCommhubMcpDoctor("not-json")).toThrow("invalid JSON");
});

it("derives an opaque path segment and rejects dot identities", () => {
expect(grokCliStateKey("n_grokcli215")).toMatch(/^node-[a-f0-9]{24}$/);
expect(grokCliStateKey("node/../../escape")).toMatch(/^node-[a-f0-9]{24}$/);
Expand Down Expand Up @@ -780,6 +830,7 @@ describe("prepareGrokCliHome", () => {
writeFileSync(commhubServer, "// reviewed commhub MCP server\n", { mode: 0o600 });
writeFileSync(commhubEnv, "COMMHUB_TOKEN=ntok_test\n", { mode: 0o600 });
const commhubMcp = {
command: realpathSync(process.execPath),
serverPath: commhubServer,
envFile: commhubEnv,
alias: "指挥狗",
Expand Down Expand Up @@ -808,7 +859,7 @@ describe("prepareGrokCliHome", () => {
expect(config).toContain('[ui]\ndefault_selected_permission = "always_allow_all_sessions"');
expect(config).toContain("remember_tool_approvals = true");
expect(config).toContain("[mcp_servers.commhub]");
expect(config).toContain('command = "bun"');
expect(config).toContain(`command = ${JSON.stringify(realpathSync(process.execPath))}`);
const stagedServer = join(stateHome, "runtime-mcp", "node-server.js");
const stagedEnv = join(dirname(dirname(stateHome)), ".anet-grok-credentials", basename(stateHome), ".env");
expect(config).toContain(`args = [${JSON.stringify(stagedServer)}]`);
Expand Down Expand Up @@ -938,7 +989,13 @@ describe("prepareGrokCliHome", () => {
projectCwd: project,
useLeader: true,
};
const valid = { serverPath, envFile, alias: "node-a", resumeId: "grok-cli-n_a" };
const valid = {
command: realpathSync(process.execPath),
serverPath,
envFile,
alias: "node-a",
resumeId: "grok-cli-n_a",
};

expect(() => prepareGrokCliHome({ ...base, commhubMcp: valid })).not.toThrow();
const config = readFileSync(join(stateHome, "config.toml"), "utf8");
Expand Down
70 changes: 66 additions & 4 deletions agent-node/src/runtime/grok-build-cli-home.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createHash, randomBytes } from "crypto";
import { spawn } from "child_process";
import {
accessSync,
chmodSync,
closeSync,
constants,
Expand All @@ -22,7 +23,7 @@ import {
unlinkSync,
writeFileSync,
} from "fs";
import { basename, dirname, isAbsolute, join, parse, relative, resolve } from "path";
import { basename, delimiter, dirname, isAbsolute, join, parse, relative, resolve } from "path";
import { homedir } from "os";
import { buildGrokHelperEnv } from "./grok-child-env";
import {
Expand All @@ -43,12 +44,65 @@ export interface PrepareGrokCliHomeOptions {
}

export interface GrokCommhubMcpConfig {
/** Absolute, preflighted Bun executable used by the runtime-owned MCP. */
command: string;
serverPath: string;
envFile: string;
alias: string;
resumeId: string;
}

/**
* Resolve the runtime-owned CommHub MCP executable before the TUI can become
* ready. A bare `command = "bun"` makes recovery depend on whichever PATH
* happened to launch tmux, producing a false-healthy inbound-only node.
*/
export function resolveGrokCommhubMcpCommand(command: string, pathEnv: string): string {
if (!command || command.includes("\0") || command.includes("\r") || command.includes("\n")) {
throw new Error("grok copresence CommHub MCP command is invalid");
}
const candidates = isAbsolute(command)
? [command]
: pathEnv.split(delimiter).filter(Boolean).map((directory) => resolve(directory, command));
for (const candidate of candidates) {
try {
const canonical = realpathSync(candidate);
if (!statSync(canonical).isFile()) continue;
accessSync(canonical, constants.X_OK);
return canonical;
} catch {}
}
throw new Error("grok copresence CommHub MCP command could not be resolved or executed");
}

/** Fail closed unless Grok actually starts and handshakes the one MCP server. */
export function assertGrokCommhubMcpDoctor(doctorJson: string): void {
let report: any;
try {
report = JSON.parse(doctorJson);
} catch {
throw new Error("grok copresence CommHub MCP doctor returned invalid JSON");
}
const server = Array.isArray(report?.servers)
? report.servers.find((candidate: any) => candidate?.name === "commhub")
: undefined;
const requiredChecks = ["command found", "server started", "handshake OK", "4 tools discovered"];
const checks = Array.isArray(server?.checks) ? server.checks : [];
const missing = requiredChecks.filter((label) => !checks.some(
(check: any) => check?.label === label && check?.passed === true,
));
if (
report?.healthy_count !== 1
|| report?.failing_count !== 0
|| server?.healthy !== true
|| missing.length > 0
) {
throw new Error(
`grok copresence CommHub MCP readiness failed${missing.length ? `: ${missing.join(", ")}` : ""}`,
);
}
}

export interface GrokCliHome {
home: string;
authPath: string;
Expand Down Expand Up @@ -996,16 +1050,24 @@ function validateCommhubMcpConfig(
projectCwd: string,
): GrokCommhubMcpConfig {
const projectAnet = join(projectCwd, ".anet");
const command = resolve(config.command);
const serverPath = resolve(config.serverPath);
const envFile = resolve(config.envFile);
if (
config.serverPath !== serverPath
config.command !== command
|| config.serverPath !== serverPath
|| config.envFile !== envFile
|| serverPath !== join(projectAnet, "node-server.js")
|| envFile !== join(projectAnet, ".env")
) {
throw new Error("grok-build-cli commhub MCP paths must be the canonical project .anet artifacts");
}
try {
if (!statSync(command).isFile() || realpathSync(command) !== command) throw new Error("not canonical");
accessSync(command, constants.X_OK);
} catch {
throw new Error("grok-build-cli commhub MCP command is not a canonical executable file");
}
const uid = process.getuid?.();
for (const [path, label, expectedMode] of [
[serverPath, "server", undefined],
Expand All @@ -1028,7 +1090,7 @@ function validateCommhubMcpConfig(
throw new Error(`grok-build-cli commhub MCP ${label} is invalid`);
}
}
return { serverPath, envFile, alias: config.alias, resumeId: config.resumeId };
return { command, serverPath, envFile, alias: config.alias, resumeId: config.resumeId };
}

/**
Expand Down Expand Up @@ -1388,7 +1450,7 @@ export function prepareGrokCliHome(opts: PrepareGrokCliHomeOptions): GrokCliHome
"",
...(commhubMcp ? [
"[mcp_servers.commhub]",
'command = "bun"',
`command = ${JSON.stringify(commhubMcp.command)}`,
`args = [${JSON.stringify(commhubMcp.serverPath)}]`,
`env = { ANET_COMMHUB_ENV_FILE = ${JSON.stringify(commhubMcp.envFile)}, ANET_COMMHUB_MODE = "outbound-only", COMMHUB_ALIAS = ${JSON.stringify(commhubMcp.alias)}, COMMHUB_RESUME_ID = ${JSON.stringify(commhubMcp.resumeId)} }`,
"enabled = true",
Expand Down
Loading
Loading