diff --git a/agent-node/src/runtime/grok-copresence/policy.ts b/agent-node/src/runtime/grok-copresence/policy.ts index dde513951..4468a42c2 100644 --- a/agent-node/src/runtime/grok-copresence/policy.ts +++ b/agent-node/src/runtime/grok-copresence/policy.ts @@ -30,6 +30,46 @@ export const GROK_COPRESENCE_EFFECTIVE_TOOLS = Object.freeze([ ...(GROK_COPRESENCE_REPO_READ_ENABLED ? ["read_file", "grep", "list_dir"] : []), ]); +/** + * Vendor-native effectful/control tools that must remain unavailable even + * when the pinned interactive TUI ignores the agent profile's `tools` + * inventory. This includes every non-read/non-search name observed in the + * pinned 0.2.93 AvailableCommandsUpdate inventory, plus conservative native + * aliases seen in the binary/repository. Keep the Claude-compatible aliases + * in runtime argv too, but never rely on them as translations for Grok's + * lifecycle names. + */ +export const GROK_COPRESENCE_VENDOR_DENY_TOOLS = Object.freeze([ + "run_terminal_command", + "run_terminal_cmd", + "search_replace", + "write_file", + "edit_file", + "apply_patch", + "write", + "kill_command_or_subagent", + "get_command_or_subagent_output", + "wait_commands_or_subagents", + "scheduler_create", + "scheduler_delete", + "scheduler_list", + "monitor", + "update_goal", + "enter_plan_mode", + "exit_plan_mode", + "ask_user_question", + "web_fetch", + "http_request", + "image_gen", + "image_edit", + "generate_image", + "video_gen", + "generate_video", + "browser", + "computer", + "screenshot", +]); + export const GROK_COPRESENCE_AGENT_NAME = "anet-copresence-preview"; export const GROK_COPRESENCE_AGENT_FILE = `${GROK_COPRESENCE_AGENT_NAME}.md`; export const GROK_COPRESENCE_PROFILE_MARKER = "ANET_COPRESENCE_PROFILE_V1"; diff --git a/agent-node/src/runtime/grok-copresence/profile-process.test.ts b/agent-node/src/runtime/grok-copresence/profile-process.test.ts index d0bf43e58..178abc19c 100644 --- a/agent-node/src/runtime/grok-copresence/profile-process.test.ts +++ b/agent-node/src/runtime/grok-copresence/profile-process.test.ts @@ -22,6 +22,10 @@ function probe(profile: "commhub-only" | "x-search" | "repo-read"): ProbeResult return JSON.parse(child.stdout); } +function deniedTools(result: ProbeResult): string[] { + return result.args.flatMap((value, index) => result.args[index - 1] === "--deny" ? [value] : []); +} + describe("Grok co-presence profile is pinned for the whole process", () => { test("same input yields three exact, non-overlapping process capabilities", () => { const restricted = probe("commhub-only"); @@ -67,12 +71,40 @@ describe("Grok co-presence profile is pinned for the whole process", () => { ]); for (const result of [restricted, xSearch, repoRead]) { - const denied = result.args.flatMap((value, index) => result.args[index - 1] === "--deny" ? [value] : []); + const denied = deniedTools(result); + for (const tool of [ + "run_terminal_command", "run_terminal_cmd", "search_replace", "write_file", + "edit_file", "apply_patch", "write", "kill_command_or_subagent", + "get_command_or_subagent_output", "wait_commands_or_subagents", "scheduler_create", + "scheduler_delete", "scheduler_list", "monitor", "update_goal", "enter_plan_mode", + "exit_plan_mode", "ask_user_question", "web_fetch", "http_request", "image_gen", + "image_edit", "generate_image", "video_gen", "generate_video", "browser", "computer", + "screenshot", + ]) expect(denied).toContain(tool); + for (const tool of [ + "run_terminal_command", "read_file", "search_replace", "list_dir", "grep", + "kill_command_or_subagent", "todo_write", "get_command_or_subagent_output", + "wait_commands_or_subagents", "scheduler_create", "scheduler_delete", "scheduler_list", + "monitor", "search_tool", "use_tool", "update_goal", "enter_plan_mode", "exit_plan_mode", + "ask_user_question", "web_search", "web_fetch", "image_gen", "image_edit", "video_gen", + "write", + ]) { + expect(result.tools.includes(tool) || denied.includes(tool)).toBe(true); + } expect(denied).toContain("Bash"); expect(denied).toContain("Write"); expect(denied).toContain("WebFetch"); expect(denied).toContain("Read(/runtime/private)"); expect(denied).toContain("Grep(/runtime/private/**)"); } + for (const tool of ["read_file", "grep", "list_dir", "web_search"]) { + expect(deniedTools(restricted)).toContain(tool); + } + for (const tool of ["read_file", "grep", "list_dir"]) expect(deniedTools(xSearch)).toContain(tool); + expect(deniedTools(xSearch)).not.toContain("web_search"); + expect(deniedTools(repoRead)).not.toContain("read_file"); + expect(deniedTools(repoRead)).not.toContain("grep"); + expect(deniedTools(repoRead)).not.toContain("list_dir"); + expect(deniedTools(repoRead)).toContain("web_search"); }); }); diff --git a/agent-node/src/runtime/grok-copresence/runtime.test.ts b/agent-node/src/runtime/grok-copresence/runtime.test.ts index b1a80797f..7f932253b 100644 --- a/agent-node/src/runtime/grok-copresence/runtime.test.ts +++ b/agent-node/src/runtime/grok-copresence/runtime.test.ts @@ -235,6 +235,18 @@ describe("Grok copresence launch and injection policy", () => { expect(args).toContain("--always-approve"); expect(args).not.toContain("MCPTool"); const denied = args.flatMap((value, index) => args[index - 1] === "--deny" ? [value] : []); + for (const tool of [ + "run_terminal_command", "run_terminal_cmd", "search_replace", "write_file", + "edit_file", "apply_patch", "write", "kill_command_or_subagent", + "get_command_or_subagent_output", "wait_commands_or_subagents", "scheduler_create", + "scheduler_delete", "scheduler_list", "monitor", "update_goal", "enter_plan_mode", + "exit_plan_mode", "ask_user_question", "web_fetch", "http_request", "image_gen", + "image_edit", "generate_image", "video_gen", "generate_video", "browser", "computer", + "screenshot", + ]) expect(denied).toContain(tool); + for (const tool of ["read_file", "grep", "list_dir", "web_search"]) { + expect(denied).toContain(tool); + } expect(denied).toContain("Bash"); expect(denied).toContain("Write"); expect(denied).toContain("WebFetch"); diff --git a/agent-node/src/runtime/grok-copresence/runtime.ts b/agent-node/src/runtime/grok-copresence/runtime.ts index 9f4d9f7a9..8f43d77d6 100644 --- a/agent-node/src/runtime/grok-copresence/runtime.ts +++ b/agent-node/src/runtime/grok-copresence/runtime.ts @@ -45,6 +45,8 @@ import { import { assertGrokCopresenceAgentProfile, GROK_COPRESENCE_EFFECTIVE_TOOLS, + GROK_COPRESENCE_REPO_READ_ENABLED, + GROK_COPRESENCE_VENDOR_DENY_TOOLS, GROK_COPRESENCE_WEB_SEARCH_ENABLED, } from "./policy"; import { @@ -417,10 +419,19 @@ export function buildGrokCopresenceArgs(opts: BuildGrokCopresenceArgsOptions): s // filesystem/process/web-fetch escape routes remain denied. The explicit // x-search process profile may expose general web_search; it is not a // domain-restricted network sandbox. + for (const tool of GROK_COPRESENCE_VENDOR_DENY_TOOLS) { + args.push("--deny", tool); + } + if (!GROK_COPRESENCE_REPO_READ_ENABLED) { + for (const tool of ["read_file", "grep", "list_dir"]) args.push("--deny", tool); + } + if (!GROK_COPRESENCE_WEB_SEARCH_ENABLED) args.push("--deny", "web_search"); args.push( // The shared process must read its owner-only GROK_AUTH_PATH after its // sandbox re-exec. Shell access would bypass path-specific Read/Grep/Edit // rules, so the experimental preview gives up terminal tools entirely. + // These are compatibility aliases, not translations for the exact + // vendor-native names appended above. "--deny", "Bash", "--deny", "Write", "--deny", "WebFetch", diff --git a/docs/tests/report-grok-copresence-terminal-deny.txt b/docs/tests/report-grok-copresence-terminal-deny.txt new file mode 100644 index 000000000..ba290f6cc --- /dev/null +++ b/docs/tests/report-grok-copresence-terminal-deny.txt @@ -0,0 +1,126 @@ +# Grok co-presence pinned native-tool boundary — exact-source evidence + +Date: 2026-08-13 +Base: 114967626f20c7ef036c3d0e0dab295e1f983a89 (#826 report head) +Source commit: 433b4af44bdcc09145c75b697634f74aec42a7df +Image: anet-test830:433b4af4 +Image ID / repo digest: sha256:df3b83526409d21ece60f0a5a12589e0fc0933616af617ea83abd6e1416deb1c + +## Live trigger + +A prompt-contained review explicitly prohibited tools and pinned Grok 0.2.93 rendered +`Run No-op; review is prompt-only`. Its `events.jsonl` nevertheless emitted: + +```text +permission_requested tool_name=run_terminal_command +permission_resolved tool_name=run_terminal_command decision=allow wait_ms=0 +tool_completed tool_name=run_terminal_command outcome=success +``` + +The old argv denied only the cross-runtime name `Bash`. The supervisor later failed closed with +`grok_failure:approval_boundary`, but only after the vendor reported successful terminal execution. The affected +communication-dog node is intentionally offline until a reviewed build and authorized pilot exist. + +## Pinned denominator and policy + +The generated Grok agent profile is not the complete native permission boundary: pinned 0.2.93 ignored its +interactive `tools` inventory. The captured `AvailableCommandsUpdate._meta.tools` denominator is exactly: + +```text +run_terminal_command read_file search_replace list_dir grep kill_command_or_subagent +todo_write get_command_or_subagent_output wait_commands_or_subagents +scheduler_create scheduler_delete scheduler_list monitor search_tool use_tool +update_goal enter_plan_mode exit_plan_mode ask_user_question web_search web_fetch +image_gen image_edit video_gen write +``` + +Every captured name is now either explicitly allowed by the selected process profile or explicitly denied at the +final argv boundary. The three profiles differ intentionally: + +```text +commhub-only: allow todo_write/search_tool/use_tool; deny read_file/grep/list_dir/web_search +x-search: additionally allow web_search; deny read_file/grep/list_dir +repo-read: additionally allow read_file/grep/list_dir; deny web_search +``` + +All 18 captured effectful/control names outside the union of those allowed sets are common denies. Ten additional +native aliases observed in the pinned binary/repository are preventive denies. `Bash`, `Write`, and `WebFetch` +remain compatibility aliases and are not treated as translations for vendor lifecycle names. + +Only `run_terminal_command` has been observed in a live permission lifecycle. The other entries are preventive +boundary rules, not claims that every name is reachable. + +Changed source files and SHA-256 values: + +```text +e0d51898ffaabbb8bf3b5275a55f97e6a86df3008d36491f9a446966f383ae60 agent-node/src/runtime/grok-copresence/policy.ts +699371996090a058d9a01c87581dc7dc4dd7bd03072cebb85bee936912028011 agent-node/src/runtime/grok-copresence/runtime.ts +56a308e12d94bebdf27c19e1eccc08e55096f6e769df35610fd916bb4a7a0704 agent-node/src/runtime/grok-copresence/runtime.test.ts +ff60c2cdd68cce0e09592dec0594f310200405f7f9c63d58787c10dd52e2483a agent-node/src/runtime/grok-copresence/profile-process.test.ts +``` + +## Docker evidence + +The image was built from source commit `433b4af44bdcc09145c75b697634f74aec42a7df` with +`tests/test725-agent-node-unit-ci/Dockerfile` and pinned Bun 1.3.14. + +Full non-root agent-node unit domain: + +```text +1284 pass +0 fail +4609 expect() calls +Ran 1284 tests across 91 files. [115.10s] +MUTATION_RED readable-attachment-runtime-disconnected rc=1 +RESULT: PASS +``` + +Focused zero-mutation control: + +```text +54 pass +0 fail +655 expect() calls +Ran 54 tests across 2 files. [61.63s] +``` + +Seven independent mutations ran in fresh containers. Each required a unique production target, a changed file +hash, a non-zero focused test result, and a failure naming the missing tool: + +```text +MUTATION_RED common:run_terminal_command rc=1 +MUTATION_RED common:write rc=1 +MUTATION_RED common:scheduler_create rc=1 +MUTATION_RED common:web_fetch rc=1 +MUTATION_RED common:image_edit rc=1 +MUTATION_RED profile:read_file rc=1 +MUTATION_RED profile:web_search rc=1 +``` + +The first five witness terminal, write, scheduler/control, fetch, and media classes in the common deny set. The +last two witness the profile-specific read/search boundary. A representative profile mutation produced: + +```text +Expected to contain: "read_file" +0 pass +52 filtered out +2 fail +98 expect() calls +Ran 2 tests across 2 files. [264.00ms] +RC=1 +``` + +An unrelated red, a self-red baseline, or a byte-only no-op did not count. + +## Honest limits + +- NOT COVERED: a released build has not yet been exercised against the real pinned Grok 0.2.93 vendor after + this change. A fresh-session pilot must probe prompt-only review plus terminal, write, scheduler/control, + fetch, media, profile-specific read, and profile-specific web-search cases. Denied cases must emit no + request/resolution/completion and produce no filesystem/network/scheduler side effect. +- The Docker evidence proves exact argv construction, captured-inventory coverage, profile separation, and + regression sensitivity. It does not prove the vendor honors every deny rule. +- A future Grok version requires a new exact inventory and witnessed pilot. This list is not claimed complete for + any other binary. +- This delta does not grant repo-read, merge, publish, deploy, DB, secret, or cloud authority. It does not change + any production package or node configuration.