Skip to content

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

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

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。)

@vansin

vansin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Draft,不合,只留 review。这个 PR 的能力边界建得很扎实,三条我都追到代码确认了 —— 然后发现一条没人守着的隐含前提

一、三条我核过的(都成立)

selectGrokCopresenceCapabilityProfile 是精确值匹配,不是形状匹配,而且 fail-closed:

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('… supports only the exact tools profiles …');

任何没被逐字列举的组合都抛,不是「取交集」也不是「按前缀放行」。注释那句「Co-presence deliberately does not accept a general tool allowlist」把设计意图写清楚了。

(顺带:它对顺序敏感 —— ["Grep","Read","Glob"] 会抛。这是 fail-closed 方向,安全;但值得在错误消息里提一句顺序,否则写配置的人会以为自己写对了。)

② env 是无条件覆盖的,ambient 值改不了它:

const GROK_COPRESENCE_CAPABILITY_PROFILE = GROK_COPRESENCE
  ? selectGrokCopresenceCapabilityProfile()
  : "commhub-only";
process.env[GROK_COPRESENCE_PROFILE_ENV] = GROK_COPRESENCE_CAPABILITY_PROFILE;   // ← 无条件

不是 ??=,不是「只在未设置时才设」 —— 所以 shell 里预先塞一个 repo-read 进来会被盖掉。而且 GROK_COPRESENCE 关闭时钉成最严的 commhub-only

③ 边界是进程级的,不是每轮重算的 —— 注释写着「Policy/runtime modules are dynamically imported only after this boot-time pin and never re-read the node config per turn」。这一条我实测了(见下)。

🔴 二、③ 的正确性依赖一个没有任何东西在守的前提

我把 cli.ts 里所有对 copresence 模块的引用列了出来:

:75    import { … } from "./runtime/grok-copresence/profile-selection";   ← 静态,但只导出函数,不读 env,无妨
:3513  type … = import("./runtime/grok-copresence/runtime")…               ← type import,编译期擦除
:3604  await import("./runtime/grok-copresence/runtime")                   ← 动态,> 548 ✅
:3933  await import("./runtime/grok-copresence/runtime")                   ← 动态,> 548 ✅

第 548 行(env pin)之前的静态 grok-copresence import:0 条

结论成立。 但请注意它为什么成立:因为没有人在 cli.ts 顶部静态 import policy。而 policy.ts 的顶层就是

export const GROK_COPRESENCE_CAPABILITY_PROFILE = readPinnedGrokCopresenceCapabilityProfile();

一旦有人在 cli.ts 顶部写下 import { GROK_COPRESENCE_EFFECTIVE_TOOLS } from "./runtime/grok-copresence/policy"(一个非常自然的动作),这个常量就会在 pin 之前、从 ambient env 算出来。

而它会是静默的:代码照样编译、类型照样对、单测里如果先设 env 再 import 也照样绿 —— 只有在「ambient 里恰好有值」的那台机器上才会表现出来,而表现形式是能力面被悄悄放宽或收紧。

建议加一道很便宜的门:断言 agent-node/src/cli.ts不存在指向 grok-copresence/(policy|runtime)静态 import(type import 除外)。判据是纯文本的、零依赖,和仓里 check-*.py 那批同一形状。

现在这条不变量只写在注释里。注释挡不住一个 auto-import。

三、一处值得肯定的克制

selectGrokCopresenceSandboxProfilerepo-read 映射到 strict 沙箱,而不是复用 workspace:

Repo reads require Grok's kernel-enforced strict base. Pinned 0.2.93 documents the workspace base as read-everywhere.

「打开读能力」和「限制读的范围」被当成两件事分别处理了 —— 只做前者就是把整台机器的文件交出去。这一步很容易漏。

四、和 #830 的关系

这个 PR 定义了 allow 侧(repo-readread_file/grep/list_dir),#830 定义了 deny 侧(18 个 vendor-native 名字)。我在 #830 上提的问题(use_tool 在 allow 侧,而它按名字是指向其它工具的原语)对这条链是共同的 —— 放在这里再提一次,因为 allow 集合是在本 PR 里定的。

vansin added a commit that referenced this pull request Aug 18, 2026
…1004)

* fix: fail closed on broken Grok CommHub MCP

* test: exercise Grok CommHub MCP readiness handshake

* test: add isolated Grok MCP readiness gate

* test: exercise the real Grok MCP doctor

* docs: report Grok vendor MCP readiness

* test: gate Grok readiness on product startup path

* test: keep Grok gate dependency layers source-independent

* test: anchor Grok readiness to runtime events

* docs: record exact Grok MCP readiness evidence

* fix(grok): 把 #825 的 MCP 就绪 fail-closed 落到 main(rebase + 补两处今天新门抓到的)

#825(`fix(grok): fail closed when CommHub MCP is not ready`)从 2026-08-15 起是
draft,落后 main 120 个提交,上次 CI 只跑了 10 个 check(今天一个 PR 跑 13–23 个)。
它是 grok 栈 `#825#826#830 → {#836,#867}` 的**唯一出口**,它不落地,
上面四条谁都到不了 main。

**没有 force-push 那条共享 draft 分支** —— 本 PR 从我自己的分支发,内容是它那 9 个
补丁 rebase 到 `origin/main`(`rc=0`,0 冲突)再加下面两处修补。

## 今天新合的两道门抓到了它两处

上一轮我测过「#825 引入新失败 0」,那句话当时是真的 —— 但那两道门是**今天晚些时候**
才合进 main 的,门槛抬高之后它就红了:

  1. `test-suite-registration`(#1003)
     `tests/test813-grok-mcp-readiness/` 是新增套件,没有任何 CI 会跑它。
     🔴 这条特别值得修而不是豁免:**#825 的目的就是「MCP 没就绪时 fail closed」,
     而验证这件事的套件如果不进 CI,fail-closed 这个保证就没有任何东西持续守着。**
     照 test831 的同构做法接进 qa.yml:两处 `paths` + build/run 两个 step。
     ⚠️ 它的 Dockerfile 收的 build-arg 叫 `SOURCE_COMMIT`(不是 831 的
     `TEST831_SOURCE_COMMIT`),容器里由 `ENV TEST813_SOURCE_COMMIT` 承接 ——
     照抄 831 的参数名会静默拿不到值。

  2. `doc-symbol-pins`(#1002)
     `docs/message-lifecycle.md` 把 `shouldSkipMessage` 钉在 `cli.ts#L4639`,
     而 #825 给 cli.ts 加了 54 行,真值现在是 **4662**。
     文本 `cli.ts:4639` 和 URL `#L4639` 是同一句话的两副面孔,两处都改 ——
     只改 URL 会留下渲染出来给人读的那个错数字。

## 验证

    rebase 到 origin/main            rc=0,0 冲突,9 个补丁
    test-suite-registration          rc=0   suites=198 registered=34 orphans=164 new=0
    doc-symbol-pins                  rc=0
    l1-paths-sync / qa-trigger-coverage / workflow-structure / docs-integrity /
    no-escaped-comments              全部 rc=0

`registered` 从 33 变 34、`orphans` 仍是 164 —— 新套件是接进 CI 了,不是塞进基线蒙混。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(test813): 一条从写下起就是空的变异 —— 接进 CI 的第一次运行就把它照出来了

把 test813 接进 CI(本 PR 上一个提交)之后,它在 GitHub runner 上第一次运行就红:

    MCP_READINESS_PASS tools=commhub_get_all_status,commhub_send_message,commhub_send_task,commhub_upload_file
    FAIL: mutation survived: upload-tool-removed
    MCP_READINESS_PASS tools=…(同一份列表,commhub_upload_file 仍在)

不是产品没拦住,是**变异根本没发生**:

    sed -i '/^[[:space:]]*"commhub_upload_file",[[:space:]]*$/d' agent-network/src/node-server.ts

那个模式要求整行只有 `"commhub_upload_file",`,而真实那行是
`      name: "commhub_upload_file",`(对象字面量的字段)。实测命中 **0** ——
在 origin/main 上也是 0,**它从写下那天起就没匹配过任何一行**。
套件此前不在任何 CI 里(#861 说的 164 个孤儿之一),所以没人见过它红。

删整行会破坏对象字面量语法,改成改名:

    sed -i 's/name: "commhub_upload_file"/name: "commhub_upload_file_MUT"/' …

干跑验证:文件确实变了(`243:      name: "commhub_upload_file_MUT",`),已还原。

## 顺带堵住这一类,不只这一条

给 `expect_red` 加空变异防护:跑完变异命令后,若两个源文件**都**没被改动就直接判红,
并说清楚是「sed 模式和源码对不上」而不是「产品没拦住」。

这两种结论指向完全不同的下一步 —— **去改产品 vs 去改测试** —— 而它们在没有这道防护
时打印出来是同一句 `mutation survived`。

同一个套件里另外三条 sed 都逐条查过,各自命中 1(其中打 fake-grok.mjs 那条还自带
`grep -Fxc … -eq 1` 基数守卫),只有这一条是空的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: t <internlmorg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
vansin added a commit that referenced this pull request Aug 18, 2026
* feat(grok): add strict repo-read copresence profile

* docs(tests): record restacked Grok repo-read evidence

* feat(grok): 把 #826 的 strict repo-read 共存档位落到 main

grok 栈 `#825#826#830 → {#836,#867}` 的第二层。#825 已由 #1004 落地
(main `89a6164e`),所以这一层现在可以直接对 main 落。

**没有 force-push #826 那条共享 draft 分支**,本 PR 从我自己的分支发。

## rebase 方式:只取它自己那两个提交

直接 `git rebase origin/main` **会冲突** —— 冲在 `436b4bdb fix: fail closed on broken
Grok CommHub MCP`,那是 **#825 自己的提交**,已经以 squash 形式在 main 上了,而 #826
的历史里还带着它。

正确做法是把它自己那部分接到 main 上:

    git rebase --onto origin/main <#825-head> <#826-head>     rc=0

它自己的两个提交:

    4496835 feat(grok): add strict repo-read copresence profile
    1149676 docs(tests): record restacked Grok repo-read evidence

结果 12 files changed, 284 insertions(+), 36 deletions(-)。

## 今天这套门抓到一处

`doc-symbol-pins`:`docs/message-lifecycle.md` 把 `shouldSkipMessage` 钉在
`cli.ts#L4662`,而本层又往 `cli.ts` 加了行,真值变成 **4669**。文本和 URL 两处都改。

🔴 **这是同一条 pin 今晚第二次漂**(#1004 刚把它从 4639 改到 4662)。
往 `cli.ts` 这种热点文件钉**行号**,几乎每个碰它的 PR 都会把它顶漂 ——
改成只钉符号、不钉行号才是根治,但那是另一条改动,不在这里顺手做。

其余全绿:test-suite-registration(suites=198 registered=34 orphans=164 new=0)/
doc-source-pins / no-escaped-comments / no-memory-slugs / home-path-baseline /
public-script-safety / l1-paths-sync / copresence-profile-pin。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: t <internlmorg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@vansin

vansin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

这条的内容已落到 main(#1006c85dca96),因此关闭

没有 force-push 这条分支,本分支历史保持原样。落地方式是把它自己那两个提交
44968358 / 11496762)用 git rebase --onto origin/main <#825-head> 接到 main 上,
再从我自己的分支发 PR。

按内容验(不看 git cherry

git cherry 在 squash 合并后会把这条判成「未落地」——这是 #825 关闭时踩过并写清楚的:
squash 后 main 上是一个提交,patch-id 和分支上那些各自的对不上。所以逐行看
「本分支新增的行,在 main 上找不找得到」:

文件 新增行 在 main 上找不到
agent-network/src/grok-copresence-disclosure.ts 11 0
agent-network/src/grok-copresence-disclosure.test.ts 15 0
agent-node/src/cli.ts 16 0
agent-node/src/runtime/grok-build-cli-home.ts 13 0
agent-node/src/runtime/grok-build-cli-home.test.ts 8 0
agent-node/src/runtime/grok-copresence/policy.ts 8 0
agent-node/src/runtime/grok-copresence/profile-selection.ts 25 0
agent-node/src/runtime/grok-copresence/profile-selection.test.ts 13 0
agent-node/src/runtime/grok-copresence/profile-process-probe.ts 13 0
agent-node/src/runtime/grok-copresence/profile-process.test.ts 30 0
agent-node/src/runtime/grok-copresence/runtime.ts 1 0
docs/tests/report-grok-copresence-repo-read-stage2.txt 95 0

13 个文件、248 行新增,一行不缺。

落地时改的一处

doc-symbol-pins 抓到 docs/message-lifecycle.mdshouldSkipMessage 钉在
cli.ts#L4662,而本层又往 cli.ts 加了行,真值变成 4669。文本和 URL 两处都改。

(我在 #1006 正文里曾写「改成只钉符号不钉行号才是根治」,那句已撤回 ——
实测去掉 #L 会让这条 pin 整个掉出那道门的正则,扫到 15→14 / 判定 5→4 / rc 1→0
是把覆盖删掉而不是修好。详见 https://github.com/sleep2agi/agent-network/pull/1006#issuecomment-5329877912)

栈的后续

#830 → {#836, #867} 三层的落地手法已在本地干跑验证:

#830  git rebase --onto <main> <#826-head> <#830-head>   rc=0,7 个提交,七道门全绿
#867  同样形状                                            rc=0,7 个提交
#836  🔴 同样形状会失败 —— 它的区间有 49 个提交,其中 48 个已经在 main 上
      (分支上有过一次 merge main)。改用 cherry-pick 它自己那 1 个 fc221fc5,rc=0

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