Skip to content
Draft
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
17 changes: 16 additions & 1 deletion agent-network/src/grok-copresence-disclosure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,23 @@ describe("grok co-presence disclosure", () => {
expect(text).not.toContain("No filesystem, shell, web,");
});

test("repo-read profile reports only sandboxed project reads", () => {
const disclosure = grokCopresenceDisclosure(["Read", "Grep", "Glob"], "new");
const text = disclosure.lines.join("\n");
expect(disclosure.profile).toBe("repo-read");
expect(text).toContain("[todo_write,search_tool,use_tool,read_file,grep,list_dir]");
expect(text).toContain("strict sandbox");
expect(text).toContain("essential system paths");
expect(text).toContain("protected credential paths");
expect(text).toContain("shell, writes, web/media");
expect(text).not.toContain("web_search is enabled");
});

test("near-match tools are disclosed as invalid rather than a reviewed profile", () => {
for (const tools of [["websearch"], ["WebSearch", "Bash"], [" WebSearch"]]) {
for (const tools of [
["websearch"], ["WebSearch", "Bash"], [" WebSearch"],
["Read", "Glob", "Grep"], ["Read", "Grep"],
]) {
const disclosure = grokCopresenceDisclosure(tools, "configured");
expect(disclosure.profile).toBe("invalid");
expect(disclosure.lines.join("\n")).toContain("startup will fail closed");
Expand Down
14 changes: 11 additions & 3 deletions agent-network/src/grok-copresence-disclosure.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
export type GrokCopresenceSessionDisclosure = "configured" | "new" | "resume";

export type GrokCopresenceDisclosure = {
profile: "commhub-only" | "x-search" | "invalid";
profile: "commhub-only" | "x-search" | "repo-read" | "invalid";
lines: readonly string[];
};

/**
* Describe only the two exact tool profiles accepted by the pinned Grok TUI
* Describe only the exact tool profiles accepted by the pinned Grok TUI
* runtime. This is deliberately exact: a near-miss must never be presented as
* either reviewed capability set.
*/
Expand All @@ -16,6 +16,10 @@ export function grokCopresenceDisclosure(
): GrokCopresenceDisclosure {
const configured = Array.isArray(tools) ? tools : [];
const xSearch = configured.length === 1 && configured[0] === "WebSearch";
const repoRead = configured.length === 3
&& configured[0] === "Read"
&& configured[1] === "Grep"
&& configured[2] === "Glob";
const defaultProfile = configured.length === 0;

const lines: string[] = [];
Expand All @@ -24,14 +28,18 @@ export function grokCopresenceDisclosure(
profile = "x-search";
lines.push("Configured profile: x-search; fixed tools: [todo_write,search_tool,use_tool,web_search].");
lines.push("General web_search is enabled; WebFetch, filesystem, shell, media, project/host MCP, and subagents remain unavailable.");
} else if (repoRead) {
profile = "repo-read";
lines.push("Configured profile: repo-read; fixed tools: [todo_write,search_tool,use_tool,read_file,grep,list_dir].");
lines.push("Read-only filesystem access uses Grok's strict sandbox (project tree plus essential system paths); protected credential paths, shell, writes, web/media, and subagents remain unavailable.");
} else if (defaultProfile) {
profile = "commhub-only";
lines.push("Configured profile: commhub-only; fixed tools: [todo_write,search_tool,use_tool].");
lines.push("No filesystem, shell, web, media, project/host MCP, or subagents.");
} else {
profile = "invalid";
lines.push("Configured tools do not match a supported exact Grok co-presence profile; startup will fail closed.");
lines.push("Supported profiles are [] and [WebSearch]; custom or near-match tool names are not accepted.");
lines.push('Supported profiles are [], [WebSearch], and [Read,Grep,Glob]; custom, reordered, or near-match tool names are not accepted.');
}

if (session === "new") {
Expand Down
23 changes: 16 additions & 7 deletions agent-node/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { resolveGrokAcpTimeout } from "./runtime/grok-build-acp/timeout-resolve"
import {
GROK_COPRESENCE_PROFILE_ENV,
selectGrokCopresenceCapabilityProfile,
selectGrokCopresenceSandboxProfile,
} from "./runtime/grok-copresence/profile-selection";
import {
defaultNpmInstall,
Expand Down Expand Up @@ -549,7 +550,7 @@ if (GROK_COPRESENCE) {
console.warn(
`[agent-node] EXPERIMENTAL/DANGEROUS grok-build-cli co-presence is enabled `
+ `(process profile=${GROK_COPRESENCE_CAPABILITY_PROFILE}); the shared human TUI must receive `
+ "tasks only from trusted senders. MCP is CommHub-only; WebSearch is available only in the explicit x-search profile.",
+ "tasks only from trusted senders. MCP is CommHub-only; WebSearch and repo reads are available only in their explicit profiles.",
);
}
// Default 50 turns. The old default of 5 was way too low — Claude Agent SDK
Expand Down Expand Up @@ -3569,7 +3570,7 @@ async function ensureGrokCopresenceRuntime(): Promise<GrokCopresenceSession> {
);
}
// The generic tools option is not forwarded. It was already reduced at
// process boot to one of two exact profiles; any other value failed before
// process boot to one exact runtime-owned profile; any other value failed before
// the runtime module was loaded.
if (
MAX_TURNS_CLI !== undefined
Expand Down Expand Up @@ -3768,9 +3769,15 @@ async function ensureGrokCopresenceRuntime(): Promise<GrokCopresenceSession> {
alias: currentAlias(),
model: MODEL || undefined,
agentProfile: grokCliHome.copresenceAgentProfile,
// The runtime always approves its fixed three-tool profile. Filesystem,
// shell, web, host/project MCP and subagent capabilities remain absent.
sandboxProfile: grokCliHome.workspaceProfile,
// Repo-read is the only profile with filesystem tools. Pinned 0.2.93
// documents workspace as read-everywhere, so repo-read must use the
// kernel-enforced strict base (CWD + essential system paths). A resumed
// workspace session cannot change sandbox and therefore requires an
// explicit new session before repo-read can start.
sandboxProfile: selectGrokCopresenceSandboxProfile(
GROK_COPRESENCE_CAPABILITY_PROFILE,
grokCliHome,
),
protectedPaths: [
grokCliHome.home,
grokCliHome.commhubCredentialDir || "",
Expand Down Expand Up @@ -5876,15 +5883,17 @@ if (AUTH_TOKEN) {
}

// #101 fix: log resolved toolset shape. Co-presence reduces the generic config
// to one of two runtime-owned process profiles verified for the pinned TUI.
// to one runtime-owned process profile verified for the pinned TUI.
const requestedToolsSummary =
Array.isArray(TOOLS)
? (TOOLS.length ? `[${TOOLS.join(",")}]` : "(none)")
: "all (Claude Code preset — built-in: WebFetch/WebSearch/Bash/Read/Write/Edit/Glob/Grep/Task/...)";
log(` tools: ${GROK_COPRESENCE
? GROK_COPRESENCE_CAPABILITY_PROFILE === "x-search"
? "fixed x-search profile [todo_write,search_tool,use_tool,web_search] (general web; no web-fetch/filesystem/shell/media/subagents)"
: "fixed commhub-only profile [todo_write,search_tool,use_tool] (no filesystem/shell/web/media/subagents)"
: GROK_COPRESENCE_CAPABILITY_PROFILE === "repo-read"
? "fixed repo-read profile [todo_write,search_tool,use_tool,read_file,grep,list_dir] (strict CWD reads; no shell/write/web/media/subagents)"
: "fixed commhub-only profile [todo_write,search_tool,use_tool] (no filesystem/shell/web/media/subagents)"
: requestedToolsSummary}`);
log(` channels:${[
TELEGRAM_CHANNELS.length ? `telegram(${TELEGRAM_CHANNELS.map(ch => ch.dir).join(",")})` : "",
Expand Down
8 changes: 8 additions & 0 deletions agent-node/src/runtime/grok-build-cli-home.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ describe("prepareGrokCliHome", () => {

expect(first).toEqual(second);
expect(first.readOnlyProfile).toMatch(/^anet-[a-f0-9]{24}-read-only$/);
expect(first.strictProfile).toMatch(/^anet-[a-f0-9]{24}-strict$/);
expect(first.authPath).toBe(join(sourceHome, "auth.json"));
expect(first.oidcIssuer).toBe("https://auth.example.test");
expect(first.oidcClientId).toBe("client-123");
Expand All @@ -165,6 +166,7 @@ describe("prepareGrokCliHome", () => {
expect(sandbox).toContain(secretDir);
expect(sandbox).toContain(join(sourceHome, "auth.json"));
expect(sandbox).toContain('extends = "read-only"');
expect(sandbox).toContain('extends = "strict"');
expect(sandbox).toContain('extends = "workspace"');
});

Expand Down Expand Up @@ -871,6 +873,12 @@ describe("prepareGrokCliHome", () => {
expect(statSync(stagedEnv).mode & 0o777).toBe(0o600);
expect(config).toContain('COMMHUB_ALIAS = "指挥狗"');
expect(config).not.toContain("ntok_test");
const strictSandbox = readFileSync(join(stateHome, "sandbox.toml"), "utf8");
const strictBlock = strictSandbox.split('extends = "strict"')[1]?.split("[profiles.")[0] || "";
const command = config.match(/^command = (.+)$/m)?.[1] || "";
expect(strictBlock).toContain(JSON.stringify(sourceHome));
expect(strictBlock).toContain(JSON.stringify(dirname(stagedEnv)));
expect(strictBlock).toContain(command);
expect(readFileSync(join(stateHome, "requirements.toml"), "utf8"))
.toBe("[ui]\ndisable_bypass_permissions_mode = false\n");
const trustStore = join(stateHome, "trusted_folders.toml");
Expand Down
14 changes: 13 additions & 1 deletion agent-node/src/runtime/grok-build-cli-home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export interface GrokCliHome {
oidcIssuer?: string;
oidcClientId?: string;
readOnlyProfile: string;
strictProfile: string;
workspaceProfile: string;
/** Absolute runtime-owned profile passed through the TUI-effective --agent flag. */
copresenceAgentProfile?: string;
Expand Down Expand Up @@ -1399,6 +1400,7 @@ export function prepareGrokCliHome(opts: PrepareGrokCliHomeOptions): GrokCliHome
}

const readOnlyProfile = `${profileId}-read-only`;
const strictProfile = `${profileId}-strict`;
const workspaceProfile = `${profileId}-workspace`;
const existingSecretPaths = resolvedDenyPaths.filter((path) => existsSync(path));
if (!existingSecretPaths.length) {
Expand Down Expand Up @@ -1509,7 +1511,7 @@ export function prepareGrokCliHome(opts: PrepareGrokCliHomeOptions): GrokCliHome

if (opts.useLeader === true) {
// This runtime deliberately uses the pinned CLI's always-approve mode for
// its fixed three-tool profile. Keep the user-tier requirements file from
// its exact runtime-owned profile. Keep the user-tier requirements file from
// accidentally disabling that mode.
writeGeneratedFile(join(stateHome, "requirements.toml"), [
"[ui]",
Expand All @@ -1523,6 +1525,15 @@ export function prepareGrokCliHome(opts: PrepareGrokCliHomeOptions): GrokCliHome
'extends = "read-only"',
`deny = [${denyToml}]`,
"",
`[profiles.${JSON.stringify(strictProfile)}]`,
'extends = "strict"',
`read_only = [${[
sourceHome,
...(commhubMcp ? [commhubMcp.command] : []),
...(stagedCommhubMcp ? [stagedCommhubMcp.credentialDir] : []),
].map((path) => JSON.stringify(path)).join(", ")}]`,
`deny = [${denyToml}]`,
"",
`[profiles.${JSON.stringify(workspaceProfile)}]`,
'extends = "workspace"',
`deny = [${denyToml}]`,
Expand All @@ -1549,6 +1560,7 @@ export function prepareGrokCliHome(opts: PrepareGrokCliHomeOptions): GrokCliHome
...(oidcIssuer && oidcClientId ? { oidcIssuer, oidcClientId } : {}),
...(stagedCommhubMcp ? { commhubCredentialDir: stagedCommhubMcp.credentialDir } : {}),
readOnlyProfile,
strictProfile,
workspaceProfile,
...(opts.useLeader === true ? { copresenceAgentProfile } : {}),
};
Expand Down
11 changes: 8 additions & 3 deletions agent-node/src/runtime/grok-copresence/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ import { readPinnedGrokCopresenceCapabilityProfile } from "./profile-selection";
* therefore uses one runtime-owned agent profile and verifies the effective
* request inventory independently. The two MCP dispatcher tools are useful
* only because startup separately proves that exactly one runtime-owned
* `commhub` server was discovered. Filesystem, process, web/media, subagent,
* and scheduler tools remain absent.
* `commhub` server was discovered. The repo-read profile adds only the three
* read-only project tools; process, write, web/media, subagent, and scheduler
* tools remain absent, while protected credential paths stay hard-denied.
*/
export const GROK_COPRESENCE_CAPABILITY_PROFILE = readPinnedGrokCopresenceCapabilityProfile();
export const GROK_COPRESENCE_WEB_SEARCH_ENABLED = GROK_COPRESENCE_CAPABILITY_PROFILE === "x-search";
export const GROK_COPRESENCE_REPO_READ_ENABLED = GROK_COPRESENCE_CAPABILITY_PROFILE === "repo-read";
export const GROK_COPRESENCE_EFFECTIVE_TOOLS = Object.freeze([
"todo_write",
"search_tool",
"use_tool",
...(GROK_COPRESENCE_WEB_SEARCH_ENABLED ? ["web_search"] : []),
...(GROK_COPRESENCE_REPO_READ_ENABLED ? ["read_file", "grep", "list_dir"] : []),
]);

export const GROK_COPRESENCE_AGENT_NAME = "anet-copresence-preview";
Expand All @@ -44,7 +47,9 @@ export function renderGrokCopresenceAgentProfile(): string {
"---",
GROK_COPRESENCE_WEB_SEARCH_ENABLED
? `${GROK_COPRESENCE_PROFILE_MARKER}: Answer the current user directly. The runtime-owned outbound-only commhub MCP integration and general web_search are available; do not claim inbound CommHub, lifecycle/presence ownership, filesystem, shell, web-fetch/media, or subagent access. Web search is not an x.com-only network sandbox.`
: `${GROK_COPRESENCE_PROFILE_MARKER}: Answer the current user directly. Only the runtime-owned outbound-only commhub MCP integration is available; do not claim inbound CommHub, lifecycle/presence ownership, filesystem, shell, web/media, or subagent access.`,
: GROK_COPRESENCE_REPO_READ_ENABLED
? `${GROK_COPRESENCE_PROFILE_MARKER}: Answer the current user directly. The runtime-owned outbound-only commhub MCP integration plus read_file, grep, and list_dir are available under Grok's strict sandbox: project tree plus essential system paths only; protected credential paths remain denied. Do not claim shell, write/edit, web/media, lifecycle/presence ownership, or subagent access.`
: `${GROK_COPRESENCE_PROFILE_MARKER}: Answer the current user directly. Only the runtime-owned outbound-only commhub MCP integration is available; do not claim inbound CommHub, lifecycle/presence ownership, filesystem, shell, web/media, or subagent access.`,
"",
].join("\n");
}
Expand Down
20 changes: 14 additions & 6 deletions agent-node/src/runtime/grok-copresence/profile-process-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import {
GROK_COPRESENCE_EFFECTIVE_TOOLS,
renderGrokCopresenceAgentProfile,
} from "./policy";
import { selectGrokCopresenceSandboxProfile } from "./profile-selection";

const sandboxProfile = selectGrokCopresenceSandboxProfile(
GROK_COPRESENCE_CAPABILITY_PROFILE,
{ workspaceProfile: "anet-test232-workspace", strictProfile: "anet-test232-strict" },
);

const args = buildGrokCopresenceArgs({
cwd: "/workspace/project",
sessionId: "23223223-2232-4232-8232-232232232232",
resume: false,
leaderSocket: "/tmp/anet-test232/leader.sock",
agentProfile: "/runtime/anet-copresence-preview.md",
sandboxProfile: "anet-test232-workspace",
sandboxProfile,
protectedPaths: ["/runtime/private"],
});
const automaticTool = (tool: string, turnOwner: "human" | "network") => isGrokPreviewAutomaticResolution({
Expand All @@ -33,14 +39,16 @@ const automaticTool = (tool: string, turnOwner: "human" | "network") => isGrokPr
process.stdout.write(JSON.stringify({
profile: GROK_COPRESENCE_CAPABILITY_PROFILE,
tools: GROK_COPRESENCE_EFFECTIVE_TOOLS,
sandboxProfile,
args,
renderedProfile: renderGrokCopresenceAgentProfile(),
automaticWebSearch: {
human: automaticTool("web_search", "human"),
network: automaticTool("web_search", "network"),
},
webSearchNearMisses: [
automaticTools: Object.fromEntries(GROK_COPRESENCE_EFFECTIVE_TOOLS.map((tool) => [tool, {
human: automaticTool(tool, "human"),
network: automaticTool(tool, "network"),
}])),
toolNearMisses: [
"web_search2", "WebSearch", " web_search", "web_search ", "web-search",
"web_search\n", "web_search", "not_web_search",
"read_file2", "Read", "read-file", " grep", "list_dir ", "list_directory",
].map((tool) => [tool, automaticTool(tool, "network")]),
}) + "\n");
40 changes: 32 additions & 8 deletions agent-node/src/runtime/grok-copresence/profile-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { GROK_COPRESENCE_PROFILE_ENV } from "./profile-selection";
interface ProbeResult {
profile: string;
tools: string[];
sandboxProfile: string;
args: string[];
renderedProfile: string;
automaticWebSearch: { human: boolean; network: boolean };
webSearchNearMisses: Array<[string, boolean]>;
automaticTools: Record<string, { human: boolean; network: boolean }>;
toolNearMisses: Array<[string, boolean]>;
}

function probe(profile: "commhub-only" | "x-search"): ProbeResult {
function probe(profile: "commhub-only" | "x-search" | "repo-read"): ProbeResult {
const child = spawnSync(process.execPath, [join(import.meta.dir, "profile-process-probe.ts")], {
encoding: "utf8",
env: { ...process.env, [GROK_COPRESENCE_PROFILE_ENV]: profile },
Expand All @@ -22,33 +23,56 @@ function probe(profile: "commhub-only" | "x-search"): ProbeResult {
}

describe("Grok co-presence profile is pinned for the whole process", () => {
test("same input yields two exact, non-overlapping process capabilities", () => {
test("same input yields three exact, non-overlapping process capabilities", () => {
const restricted = probe("commhub-only");
const xSearch = probe("x-search");
const repoRead = probe("repo-read");

expect(restricted.profile).toBe("commhub-only");
expect(restricted.tools).toEqual(["todo_write", "search_tool", "use_tool"]);
expect(restricted.sandboxProfile).toBe("anet-test232-workspace");
expect(restricted.args).toContain("--disable-web-search");
expect(restricted.renderedProfile).not.toContain(" - web_search");
expect(restricted.automaticWebSearch).toEqual({ human: false, network: false });
expect(restricted.automaticTools).toEqual({
todo_write: { human: true, network: true },
search_tool: { human: true, network: true },
use_tool: { human: true, network: true },
});

expect(xSearch.profile).toBe("x-search");
expect(xSearch.tools).toEqual(["todo_write", "search_tool", "use_tool", "web_search"]);
expect(xSearch.sandboxProfile).toBe("anet-test232-workspace");
expect(xSearch.args).not.toContain("--disable-web-search");
expect(xSearch.renderedProfile).toContain(" - web_search");
expect(xSearch.renderedProfile).toContain("not an x.com-only network sandbox");
expect(xSearch.automaticWebSearch).toEqual({ human: true, network: true });
expect(xSearch.webSearchNearMisses).toEqual([
expect(xSearch.automaticTools.web_search).toEqual({ human: true, network: true });

expect(repoRead.profile).toBe("repo-read");
expect(repoRead.tools).toEqual([
"todo_write", "search_tool", "use_tool", "read_file", "grep", "list_dir",
]);
expect(repoRead.sandboxProfile).toBe("anet-test232-strict");
expect(repoRead.args).toContain("--disable-web-search");
expect(repoRead.renderedProfile).toContain("strict sandbox");
for (const tool of ["read_file", "grep", "list_dir"]) {
expect(repoRead.automaticTools[tool]).toEqual({ human: true, network: true });
}

expect(repoRead.toolNearMisses).toEqual([
["web_search2", false], ["WebSearch", false], [" web_search", false],
["web_search ", false], ["web-search", false], ["web_search\n", false],
["web_search", false], ["not_web_search", false],
["read_file2", false], ["Read", false], ["read-file", false], [" grep", false],
["list_dir ", false], ["list_directory", false],
]);

for (const result of [restricted, xSearch]) {
for (const result of [restricted, xSearch, repoRead]) {
const denied = result.args.flatMap((value, index) => result.args[index - 1] === "--deny" ? [value] : []);
expect(denied).toContain("Bash");
expect(denied).toContain("Write");
expect(denied).toContain("WebFetch");
expect(denied).toContain("Read(/runtime/private)");
expect(denied).toContain("Grep(/runtime/private/**)");
}
});
});
Loading
Loading