fix(tools): validate_data, review and verify go through the read guard

resolve_guarded_read_path is the single entry for in-process readers
that hand file content, or something derived from it, to a model. It
applies the deny-list to the path as written, resolves it, refuses
Codewhale credential and config files, and applies the deny-list again
to the resolved path. d87f4c33ca moved pandoc_convert and image_ocr onto
it; three readers were missed:

- validate_data (load_input_source) read the file with only
  resolve_path. Its TOML error prints the source line, so
  `validate_data {path: ".env", format: "toml"}` could return a dotenv
  line to the model. The tool is read-only and auto-approved, so it ran
  without a prompt even in Plan mode and for read-only workers, where
  no shell exists.
- review (resolve_file_target) and verify (gather_files) embedded the
  whole file in a provider request as a Text block. Model-bound
  redaction masks only ToolResult blocks, so nothing redacted it.

Each now calls crate::tools::file::resolve_guarded_read_path with its
tool name. A denied path fails with the same error `read` gives; verify
reports it per file through its existing error branch.

Found by the 2026-10-17 fail-open hunt (verifier confidence high for
both). Receipts: CW/artifacts/failopen-hunt-20261017/round2.json.

Verification: the scripts/dev-test.sh tui tools:: ... run described in
92f900edf0 covers these modules (4819 passed; the failures there are
unrelated to these files). clippy with the CI flags and fmt: clean.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-17 16:18:00 -07:00
1 parent 5d1c8adf30
commit 4fa8fdda02
5 files changed
+14 -3

No files matched your search

+5
View File
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- `validate_data`, `review` and `verify` honor the read deny-list, as `read`
does. They read files such as a workspace `.env`, or in trust mode
`~/.aws/credentials`, without the check. `validate_data`'s TOML parse
error could quote a line of such a file back to the model, and `review`
and `verify` sent the whole file to the provider.
- Commands that start outside the shell tool now get the same sandbox as
shell commands:
- `terminal_*` PTY sessions used a default sandbox with no credential
+5
View File
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- `validate_data`, `review` and `verify` honor the read deny-list, as `read`
does. They read files such as a workspace `.env`, or in trust mode
`~/.aws/credentials`, without the check. `validate_data`'s TOML parse
error could quote a line of such a file back to the model, and `review`
and `verify` sent the whole file to the provider.
- Commands that start outside the shell tool now get the same sandbox as
shell commands:
- `terminal_*` PTY sessions used a default sandbox with no credential
+1 -1
View File
@@ -1721,7 +1721,7 @@ async fn resolve_review_source(
}
fn resolve_file_target(target: &str, context: &ToolContext) -> Result<ReviewSource, ToolError> {
let path = context.resolve_path(target)?;
let path = crate::tools::file::resolve_guarded_read_path(context, target, "review")?;
if !path.is_file() {
return Err(ToolError::invalid_input(format!(
"Target is not a file: {}",
+2 -1
View File
@@ -124,7 +124,8 @@ fn load_input_source(
)),
(None, None) => Err(ToolError::missing_field("path or content")),
(Some(path), None) => {
let resolved = context.resolve_path(path)?;
let resolved =
crate::tools::file::resolve_guarded_read_path(context, path, "validate_data")?;
let raw_content = if context.features.enabled(crate::features::Feature::DataHost) {
use std::io::Read;
let mut content = String::new();
+1 -1
View File
@@ -720,7 +720,7 @@ fn gather_files(files: &[String], context: &ToolContext) -> Vec<EvidenceBlock> {
if raw.is_empty() {
continue;
}
match context.resolve_path(raw) {
match crate::tools::file::resolve_guarded_read_path(context, raw, "verify") {
Ok(path) => match std::fs::read_to_string(&path) {
Ok(content) => {
let display = path