fix(server): ACP agents can read files under on-request approval - #13584
Conversation
Reads now follow the sandbox alone and never ask. Under on-request or approval-required, AcpClientPolicy answered "ask" for every operation, reads included, and the client fs/read_text_file guard cannot prompt, so Grok (which routes every read through the client and never asks permission for one) could not read any file. Read-kind permission requests are auto-allowed the same way. Writes, commands and unknown kinds keep their ask/deny behavior. Read grants are removed since nothing consults them. The Grok tool_call_read_only_on_request fixture is re-recorded live and now asserts T3 serves the read-back of the approved write. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR changes production ACP authorization semantics so file reads and searches bypass approval under on-request/approval-required sessions, while writes and commands remain gated. The change is well-scoped and tested but alters a sensitive access-control boundary and existing product behavior. You can add or adjust custom eligibility rules. Learn more. |
f8b8634
into
t3code/codex-turn-mapping
Under on-request or approval-required approval, T3 refused every file read from ACP agents.
acpOperationDispositionreturnedaskfor every operation, reads included, whenever the approval policy wasn'tnever. The client-sidefs/read_text_fileguard can't prompt, so anaskread passed only if an earlier approval grant covered that path. Anything else was denied. T3 advertisesfs.readTextFile: true, and Grok then routes every read through the client without asking permission first. So under on-request, Grok couldn't read any file, including one it had just been approved to write.Decision
Reads follow the sandbox and never ask. Approval policy governs writes and commands, not reads. Codex and Claude on-request work the same way: they ask before writes and commands, not reads.
read,searchandthink(the kinds theneverpath already treated as reads) are allowed under read-only, workspace-write, full-access, external and unset sandboxes, whatever the approval policy. An unrecognised sandbox type still denies.fs/read_text_file: served without a grant. The guard now only allows or denies.session/request_permissionfor a read-kind tool call: auto-allowed, even though the provider chose to ask. Allowing it is the consistent answer, since the same read through the client fs path would be served without a prompt anyway.neverpath (a workspace-write read outside the workspace was already allowed), so nothing becomes readable thatneverdidn't already allow.fetchand unknown kinds keep their ask/deny behavior.allowsRead, thefile-readbranch ofrecordApproval). Reads never reachasknow, so nothing consults them.Grok source (xai-org/grok-build @ f0e3be1)
crates/codegen/xai-grok-shell/src/agent/mvp_agent/agent_ops.rs:4160,4193-4196: when the client advertises bothfs.readTextFileandfs.writeTextFile, Grok swaps its local filesystem forAcpSessionFs.crates/codegen/xai-grok-workspace/src/file_system/acp_fs.rs:65-74:AcpSessionFs::read_fileis a plainfs/read_text_filerequest. Nothing asks permission before it.crates/codegen/xai-grok-workspace/src/permission/manager/mod.rs:1160-1166: Grok's own permission manager pre-approvesAccessKind::ReadandGrepasSAFE_COMMAND. A read never becomes asession/request_permission.crates/codegen/xai-grok-tools/src/implementations/opencode/write/mod.rs:115-119: thewritetool reads the target first. On any error it treats the file as new (Err(_) => (false, None)), which is why Grok still wrote the file in the old recording.crates/codegen/xai-grok-tools/src/implementations/opencode/read/mod.rs:213-221: thereadtool turns a failed client read intoFileReadError("Failed to read file: …")for the model. Under the old policy, every Grok read under on-request ended there.Evidence
tool_call_read_only_on_request/grok_transcript.ndjsonwas re-recorded live (Grok 1.0.41,record-grok-acp-replay-fixture.ts) with the fix. The write still asks once (kind: "edit"→ allow-once →fs/write_text_file). The difference is in the reads:{"content":"codex app-server approval fixture."}The Grok variant now uses
assertToolCallReadOnlyOnRequestGrokOutput. It runs the shared on-request assertion (still exactly one runtime request, the write), then requires that anfs/read_text_fileresponse served the probe content and that no read was refused by the runtime policy. Against the old policy and the old recording, it fails with "T3 must serve Grok's client-mediated read of the approved file without asking".Tests changed
AcpClientPolicy.test.ts:asktoallow, which is the decision itself.allowsReadchecks, since read grants no longer exist. It keeps the check that an approved read grants neither writes nor terminals.AcpAdapterV2.test.ts: "rejects unapproved client-mediated reads in approval-required mode" became "serves client-mediated reads without approval…". It asserts the file content comes back through the adapter's real read handler.GrokAdapterV2.test.ts: the approval-requiredread→askassertion is nowread→allow, plusedit→ask, so the test still proves approval-required asks.docs/user/providers-acp.md: "approval-required threads keep asking" now says the asking applies to edits and commands, not reads.Verification
vp test run src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts -t "grok|acpRegistry": 19 passed.-t "tool_call_read_only_on_request": all 4 variants (Codex, Claude, Grok, registry) pass.vp test runon AcpClientPolicy.test.ts, GrokAdapterV2.test.ts, XAiAcpExtension.test.ts and AcpAdapterV2.test.ts: 195 passed.vp exec tsc --noEmit -p .in apps/server: 0error TS/warning TS.vp run knip:check: clean.vp fmt/vp linton touched files: no new findings (the unusedNodePathimport in AcpAdapterV2.ts and the other warnings are pre-existing).Overlap: #13579 also edits
tool_call_read_only_on_request/output.ts, in a different hunk (it removes thecontent === undefinedskip in the shared assertion). This PR only appends a Grok-specific function after it, so the two should merge cleanly.Model: Claude Opus 5.5 (Claude Code)
🤖 Generated with Claude Code