Skip to content

feat(grok): add strict repository-read co-presence profile - #826

Draft
vansin wants to merge 2 commits into
fix/813-grok-mcp-readiness-v3from
feat/grok-copresence-repo-read-v2
Draft

feat(grok): add strict repository-read co-presence profile#826
vansin wants to merge 2 commits into
fix/813-grok-mcp-readiness-v3from
feat/grok-copresence-repo-read-v2

Conversation

@vansin

@vansin vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Stacked on #825. This supersedes #820; do not merge before #825.

Goal: move the live 通信狗 from communication-only/x-search validation toward bounded AgentNetwork code review, without ACP SDK, shell, write, web, media, or subagent capability.

Frozen coordinates:

  • base source (fix(grok): fail closed when CommHub MCP is not ready #825): 8186b79de8e2f904c28bec268d93a523503a6845
  • source: 449683586a5a2ba44e99eb8c595be25d7467c967
  • report-only: 114967626f20c7ef036c3d0e0dab295e1f983a89
  • source tree: 21cd907323b374f2df6c722d314c05597ea0c093
  • unit image: sha256:d46c3dc573bba4e282750df6877fc893e490cc5d2fdfeab5c2de2e0c246dae53
  • readiness image: sha256:459b3239eae27afb6aa6a4cac7375f45c10c2df223083352187c92ca820cd60f

Exact-source Docker evidence:

This restack preserves #825's canonical Bun resolution and four-tool doctor. It intentionally removes the superseded internal bare-Bun resolver from #820.

No package was published and no live node was restarted or reconfigured. A single-node pilot remains gated on #825 review/merge, this PR's independent review, rollback coordinates, a fresh Grok session, kernel-enforcement observation, and a real read+CommHub UAT.

Full report: docs/tests/report-grok-copresence-repo-read-stage2.txt.

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

独立窄审 · PR #826 (Draft) — repo-read stacked on #825

Verdict: CLEAN — no BLOCKER, no MAJOR, no MINOR. Two informational notes at ⑦.

Reviewer: 通信IM马 (independent, read-only). Extracted PR tree via git archive origin/pr-826 → temp dir; author worktree untouched. No merge, no deploy. Images not rebuilt on my host.

Scope framing: this PR is stacked on #825 — it adds a third repo-read capability profile with an exact-match [Read, Grep, Glob] selector, a new strict Grok sandbox profile granting read-only project reads (read_file, grep, list_dir), and defense-in-depth argv-level Deny for protected paths. It also removes an older internal bare-Bun resolver in MCP staging (per report §Restack correction) so #825's canonical Bun/4-tool-doctor gate is not silently reverted.

Dependency: MUST NOT merge before #825. #825 source (8186b79d) is a strict ancestor of #826 HEAD, so if #826 merged solo it would drag in #825's changes; verified git merge-base --is-ancestor 8186b79d origin/pr-826 = true.


Provenance

value check
HEAD (report-only) 114967626f20c7ef036c3d0e0dab295e1f983a89 ✓ matches brief
source (code+test) 449683586a5a2ba44e99eb8c595be25d7467c967 ✓ matches brief
source tree 21cd907323b374f2df6c722d314c05597ea0c093 ✓ report L8 matches git rev-parse ${source}^{tree} byte-exact
parent (#825 source) 8186b79de8e2f904c28bec268d93a523503a6845 ✓ is-ancestor of pr-826 HEAD
merge-base w/ main 034f00647d42d38d5086d7fc057eb7824a441791 (== #825's base, since #825 not merged yet)
unit image ID (claim) sha256:d46c3dc573bba4... declared, not rebuilt on my host
readiness image ID (claim) sha256:459b3239eae27a... declared, not rebuilt on my host
files (base(#825 src)..HEAD) 12 (incremental scope) matches
files (main..HEAD) 19 (12 incremental + 7 from #825) expected — stacked
source→HEAD delta exactly docs/tests/report-grok-copresence-repo-read-stage2.txt ✓ report-only chain

Per-file drift base..main: 0 for every one of the 19 touched files. git merge-tree base main pr-826 = 0 conflict markers.

Report source_commit anchor honesty: report L6 declares source_commit=449683586a5a2ba44e99eb8c595be25d7467c967 — byte-match to PR source. tree=21cd9073... byte-match to git rev-parse ${source}^{tree}. No #800/#822-class stale anchor.


Item-by-item

① exact ["Read","Grep","Glob"] selector fail-closed on near-miss — CONFIRMED

selectGrokCopresenceCapabilityProfile(tools) in profile-selection.ts (new module):

if (tools === undefined || tools.length === 0) return "commhub-only";
if (tools.length === 1 && tools[0] === "WebSearch") return "x-search";
if (
  tools.length === 3
  && tools[0] === "Read"
  && tools[1] === "Grep"
  && tools[2] === "Glob"
) return "repo-read";
throw new Error('grok copresence supports only the exact tools profiles ...');

Near-miss test coverage (profile-selection.test.ts:16-21):

for (const tools of [
  ["web_search"], ["WebSearch", "WebFetch"], ["WebSearch "], ["all"], ["Read"],
  ["Read", "Glob", "Grep"], ["Read", "Grep"], ["Read", "Grep", "Glob", "WebSearch"],
]) {
  expect(() => selectGrokCopresenceCapabilityProfile(tools)).toThrow("exact tools profile");
}

All 8 near-misses throw: wrong case, extra tool, trailing whitespace, different name, subset, reordered, missing element, superset. This is exact-match discipline — cannot be tricked by whitespace, ordering, or superset/subset.

Additionally, the readPinnedGrokCopresenceCapabilityProfile env reader defaults to "commhub-only" on empty, and throws on any value other than the three allowed strings ("human-turn" test case throws with "invalid"). Both the CLI-time selector and the env-reader are fail-closed.

Selector is called ONCE at boot (per policy.ts:24 GROK_COPRESENCE_CAPABILITY_PROFILE = readPinnedGrokCopresenceCapabilityProfile()) and pinned for the whole process — resume cannot widen. Report L44 confirms: enabling repo-read requires a fresh Grok session.

② Model-visible tools: no shell/write/web/media/subagent — CONFIRMED

GROK_COPRESENCE_EFFECTIVE_TOOLS in policy.ts:

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"] : []),
]);

No bash, no write/edit, no web_fetch, no media (image/audio), no subagent.

Argv-level defense in depth (profile-process.test.ts:70-78):

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

All three profiles pass ARGV-level Deny for Bash, Write, WebFetch, plus protected-path Deny for Read(<path>) and Grep(<path>/**). Even if the profile TOML somehow allowed a broader read, Grok's argv-level Deny would reject the tool invocation.

Near-miss tool name blocking (profile-process-probe.ts + test):

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")])

Test asserts ALL 14 near-miss names return false for automaticTools — including unicode-lookalike web_search (fullwidth) and whitespace-mutated variants. Auto-resolution is fail-closed on anything other than the exact effective-tools list.

③ repo-read requires strict, named mutation red — CONFIRMED

selectGrokCopresenceSandboxProfile in profile-selection.ts:

export function selectGrokCopresenceSandboxProfile(profile, profiles) {
  return profile === "repo-read" ? profiles.strictProfile : profiles.workspaceProfile;
}

Wired in cli.ts:3776-3779:

sandboxProfile: selectGrokCopresenceSandboxProfile(
  GROK_COPRESENCE_CAPABILITY_PROFILE,
  grokCliHome,
),

Witnessed-red mutation repo-read-selector-strict-to-workspace (per report §"Named witnessed-red mutation"):

Mutation changes only the production repo-read result from the strict profile to the workspace profile. Baseline runs first; the mutation target and replacement are exact.

2 pass
1 fail
Expected: "anet-strict"
Received: "anet-workspace"
MUTATION_RED repo-read-selector-strict-to-workspace rc=1

Named-red string quotes the concrete profile names — mutation dies AT the selector assertion (not at some ambient failure). Production selector controls whether repo-read gets the strict base.

Custom strict profile TOML (grok-build-cli-home.ts:1528-1535):

[profiles."<strict>"]
extends = "strict"
read_only = [<sourceHome>, <commhubMcp.command>, <stagedCommhubMcp.credentialDir>]
deny = [<denyToml>]

Base is Grok's kernel-enforced strict (CWD + essential system paths only), with minimal read-only additions for the artifacts the MCP subprocess needs to spawn (Grok itself, Bun binary, credential env file/server script).

#825 overlap correctly preserved; no bare-Bun revival — CONFIRMED

Preservation check (git show ${source826}:agent-node/src/cli.ts | grep):

  • Line 3594: assertGrokCommhubMcpDoctor, — still imported ✓
  • Line 3597: resolveGrokCommhubMcpCommand, — still imported ✓
  • Line 3629-3630: resolveGrokCommhubMcpCommand(process.env.BUN_BIN || "bun", ...) — still called BEFORE prepareRuntime ✓
  • Line 3751: assertGrokCommhubMcpDoctor(doctor) — still asserted before spawn ✓
  • Line 3313: process.env.BUN_BIN || "bun" — bare Bun call site (this is the uploadMcpEntry handler, NOT the Grok copresence gate — per fix(grok): fail closed when CommHub MCP is not ready #825 audit this is a known tracked risk in [mcp-runtime] generated bare Bun commands fail under slim PATH #821, unchanged by this PR)

Report §Restack correction explicitly acknowledges:

The superseded source 8929fc28 resolved a bare bun again inside MCP staging. That overlapped the later #813 fix, which resolves and validates the executable before staging and passes the absolute command through the runtime boundary. This source keeps the #813 design and removes the older internal resolver while retaining repo-read's strict sandbox and read-only dependency grants.

Verified by inspection — no internal bare-Bun resolver in the copresence MCP staging path (stageCommhubMcpConfig). The stagedCommhubMcp.command inherits from the caller's already-resolved commhubMcpCommand (which came from #825's resolveGrokCommhubMcpCommand).

#825 witnessed-red mutations still all firing (per report):

MUTATION_RED upload-tool-removed
MUTATION_RED stale-three-tool-doctor
MUTATION_RED doctor-three-tools-product-path-before-tui  ← the anchored gate from #825
MUTATION_RED bun-resolver-bypassed

All four preserved. The anchored TUI ready session=[0-9a-f]{8} attach=/.+$ gate that closed the #822 minified-source-stack false-green still fires as expected.

⑤ Strict read-only doesn't leak credentials — CONFIRMED via defense in depth

Concern (worth stating explicitly): the strict profile's read_only = [sourceHome, commhubMcp.command, stagedCommhubMcp.credentialDir] grants the process file-level read access to the credential directory. Since Grok sandboxes are process-scoped (not uid-scoped) and the MCP subprocess runs under the same uid, mode 0600 on .env alone wouldn't stop model tools.

Why it's not a leak (defense in depth):

  1. File-system layer: read_only in the strict TOML grants PROCESS access — needed because the CommHub MCP subprocess (spawned by Grok) must read its own credentialDir/.env to start. Without this, MCP would fail to boot.
  2. Argv-level layer: cli.ts:3782 passes grokCliHome.commhubCredentialDir || "" into protectedPaths. Every profile (including repo-read) then gets argv --deny Read(<credentialDir>) --deny Grep(<credentialDir>/**) (verified by test: denied array contains exactly these two patterns for /runtime/private fixture).
  3. Result: MCP subprocess can read .env (needed to spawn), but MODEL TOOLS (read_file, grep) invoked by the LLM cannot read anything under credentialDir — the tool call is rejected at the argv-Deny layer before the file syscall.

Test evidence (profile-process.test.ts:76-77):

expect(denied).toContain("Read(/runtime/private)");
expect(denied).toContain("Grep(/runtime/private/**)");

protectedPaths: ["/runtime/private"] in the probe → verifies the argv-Deny pattern is emitted for every profile.

Interface comment corroborates author intent (grok-build-cli-home.ts:116-117):

/** Owner-only credential staging directory denied to model tools. */
commhubCredentialDir?: string;

One minor gap noted (informational, not blocker at ⑦b): list_dir is in the repo-read effective tool set, but the argv --deny list only explicitly names Read and Grep for protectedPaths. If a model uses list_dir on credentialDir, it would return .env, node-server.js filenames — not sensitive alone, since the dir path itself derives from random basename(stateHome), and file names are structural not credential material. But adding --deny ListDir(<path>) (or equivalent Grok flag) would be tighter defense-in-depth. Track as follow-up.

⑥ 1284/0/4406/91, readiness all PASS, 17/17 provenance — CONFIRMED (declared)

Report L28-32:

1284 pass
0 fail
4406 expect() calls
Ran 1284 tests across 91 files.
MUTATION_RED readable-attachment-runtime-disconnected rc=1
RESULT: PASS

Compared to #825's 1283/0/4373/91 baseline: +1 test file added tests, +33 expects, still 91 files (this PR added new test files but the file count in the aggregate stays at 91 — could indicate the new test files land inside src/ or the count is otherwise consistent; not a red flag, brief acknowledges baseline shifted by 1 test).

Targeted:

  • agent-node targeted: 47 pass / 0 fail / 364 expect
  • agent-network disclosure: 5 pass / 0 fail / 30 expect
  • both build/tsc: PASS

Readiness (all lines exactly as expected — preserves #825 gates):

  • MCP_READINESS_PASS with all 4 tools listed
  • 4 MUTATION_RED (upload / stale-three / doctor-three-product-path / bun-resolver)
  • 2 PRODUCT_PATH_NEGATIVE_PASS + 2 PRODUCT_PATH_RECOVERY_PASS

17/17 provenance MATCH: I did not re-verify byte-hash inside images (would require rebuilding). Structurally the source tree at 21cd9073 is byte-verifiable. The 17 files claim aligns with the number of source/fixture files copied into the two images: 12 from #826's diff + fixtures from #813/#225 + build entry points.

Additional focus (brief line "not borrowing old #820 vendor PTY evidence") — CONFIRMED

Report §Honest limits L82-83:

NOT COVERED: the earlier real pinned-Grok sandbox observations belong to source 8929fc28; they are not claimed as evidence for this restacked source.

Report explicitly does not launder old vendor evidence into the new source. Also:

NOT COVERED: a live Dashboard-origin task proving repository read plus a CommHub reply in the same TUI session.
NOT COVERED: independent adversarial review of this source/report pair.
NOT COVERED: non-Linux and multi-architecture execution.

Adequate scope acknowledgement for a pre-pilot audit gate. Report L94 explicitly states: No rollout is authorized by this report. A pilot still requires backup and rollback coordinates, a fresh Grok session, observed ProfileApplied with enforced=true, exact tool inventory, inside/outside/credential behavioral probes, and a real CommHub round trip before deeper participation.


Informational (not blocking)

⑦a Baseline test-count delta consistency

Report L36: 1284 pass ... 91 files. #825 baseline was 1283 pass ... 91 files. The +1 pass with same file count is plausible (a new test added within an existing file, or a new test file that landed under src/ matching the aggregate glob). Not a red flag; noted for cross-audit trail.

⑦b list_dir on protectedPaths

Effective tools for repo-read include list_dir, but argv-level Deny only explicitly names Read and Grep for protectedPaths. list_dir(credentialDir) would return .env, node-server.js filenames — not credential material by itself. Defense in depth would add a list_dir (or Grok's equivalent) Deny for protectedPaths. Track as follow-up if paranoid; not exploitable at present since the file names are non-sensitive and the paths derive from random state.

⑦c No CI results visible on this PR yet

GitHub CI status for origin/pr-826 — I did not fetch action runs here (would be informational only, not gate-definitive per brief). Report claims images built and passed; log digests declared (unit/targeted/selector-mutant/readiness). Anchor is 17/17 MATCH structural provenance.


Reviewer discipline (self)

Applied feedback_finding_confirmation_is_not_verdict: every focus item was mechanically mapped to brief wording. Nothing lands on BLOCKER/MAJOR/MINOR gate wording. Verdict: CLEAN.

Applied feedback_single_commit_topology_does_not_guarantee_report_source_commit: verified report's source_commit=449683586a5a... and tree=21cd9073... byte-match the actual PR source SHA and tree hash. No stale anchor.

Applied lesson from #822/#800: also verified that the "strict profile grants read_only including credentialDir" is not an actual credential leak by tracing the argv-Deny layer AND the caller's use of protectedPaths — the file-system read grant is for the MCP subprocess spawn requirements, while model tools are denied at the argv layer. Defense-in-depth is complete for Read/Grep; list_dir noted as informational.

Independent verifications on this host:

  1. git rev-parse origin/pr-826 = 11496762...
  2. git rev-parse ${source}^{tree} = 21cd9073... byte-matches report L8 ✓
  3. git merge-base --is-ancestor 8186b79d origin/pr-826 = true (stacked on fix(grok): fail closed when CommHub MCP is not ready #825 confirmed)
  4. git diff --name-only source..HEAD = report.txt only ✓
  5. git diff --name-only base(#825src)..HEAD = 12 files (incremental scope)
  6. Traced selectGrokCopresenceCapabilityProfile logic — exact-match with 8 near-miss test coverage
  7. Traced selectGrokCopresenceSandboxProfile — repo-read → strict, others → workspace
  8. Read cli.ts:3629-3630 (resolveGrokCommhubMcpCommand) and cli.ts:3751 (assertGrokCommhubMcpDoctor) — fix(grok): fail closed when CommHub MCP is not ready #825 gates preserved
  9. Read cli.ts:3313 (uploadMcpEntry bare Bun) — known tracked risk in [mcp-runtime] generated bare Bun commands fail under slim PATH #821, unchanged
  10. Read grok-build-cli-home.ts:1528-1535 (strict profile TOML) — sourceHome + Bun + credentialDir in read_only
  11. Read profile-process.test.ts:76-77 — argv --deny Read(<protected>) and --deny Grep(<protected>/**) for every profile
  12. Read report — 17/17 MATCH declared, [grok-copresence] recovery can leave inbound Hub healthy while TUI CommHub MCP is dead #813/fix(grok): fail closed when CommHub MCP is not ready #825 mutation names all present
  13. git log --oneline base..main -- <19 files> = 0 for each — no drift
  14. git merge-tree base main pr-826 | grep -cE '^<<<<<<< |^======= |^>>>>>>> ' = 0
  15. Secret sweep on 12 delta files: 0 real credentials (9+7 hits in cli.ts + grok-build-cli-home.test.ts are sanitizer regex and fixture strings, same categories as prior audits)
  16. Report §Restack correction explicitly acknowledges removal of superseded 8929fc2's internal bare-Bun resolver
  17. Report §Honest limits explicitly disclaims: old vendor PTY evidence NOT reused; no live Dashboard round trip; no independent adversarial review; no rollout authorized

No approve, no merge, no deploy. Do not merge before #825.

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

我把这个 PR 与 #836合成体放进真门跑了一遍:绿

背景:这两个 PR 都改 agent-node/src/runtime/grok-copresence/*.tscli.ts,
内容不同却不冲突 —— git 会静默合出一个谁都没审过的版本(这一类我在 #856 里量到 45 处)。
所以"各自 CI 绿"并不覆盖合成体。

真合 + 跑 test725 这道门自己的容器(不是我另写的等价物):

临时 worktree ← origin/main ← #826 ← #836     两次 merge 均干净
合成体 SOURCE_COMMIT=fc221fc5f783d0a09e1fa427860f7c88a5825919

docker build --build-arg SOURCE_COMMIT=$SHA -f tests/test725-agent-node-unit-ci/Dockerfile .
docker run --rm <image>

  # test725 — complete agent-node unit domain
  source_commit=fc221fc5f783d0a09e1fa427860f7c88a5825919
  1284 pass   0 fail   Ran 1284 tests across 91 files. [115.18s]
  RESULT: PASS                                                     退出码 0

合成体在这道门上是绿的。 这不是对两个 PR 各自的评审意见,只回答一个问题:
它们合在一起会不会把 agent-node 单测域弄坏 —— 不会。

顺带一个值得知道的对照(已单独立为 #865)

同一棵合成体,在宿主机上跑 bun test src/42 条红;进 Docker 是 0 红
那 42 条全部落在 runtime.test.ts(39)与 grok-build-cli-home.test.ts(3)——
正是这两个 PR 改的文件。原因是环境,不是代码:

TMPDIR=/tmp(默认)  35 × grok-build-cli project is busy; concurrent turns are refused
                        ← 单测去抢本机真实 grok 节点正持有的 /tmp 全局锁
TMPDIR=干净空目录     40 × leader socket path is too long for a Unix socket

所以在本机跑这两个文件的单测,结果不可用;要判断这块有没有回归,只能走 Docker 门。
本条的绿就是这么取的。

(只读:worktree 已 git worktree remove,我建的镜像已 docker rmi;
未改任何 PR、未 approve、未 merge。)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant