lint(clippy): disallow blocking std::fs on Tokio paths

Phase 2/3 of the std::fs lint initiative (Phase 1 refactors landed in
commit 9d1c0d98c).

clippy.toml additions (appended to disallowed-methods):
  std::fs::read, read_to_string, write, read_dir, copy, canonicalize
  std::fs::File::open, File::create, File::create_new
  std::fs::OpenOptions::open

File::options was deliberately excluded — it returns an OpenOptions
builder with no syscall. OpenOptions::open is where the block happens.
Non-blocking std::fs items (metadata, exists, create_dir_all, remove_*,
rename, and all std::fs types) remain legal.

Annotation policy (per updated plan):
  - Mixed async/sync production source: function- or statement-scoped
    #[expect(...)] so future accidental Tokio-path regressions in the
    same file still fire.
  - Fully-sync production source, test modules, integration tests,
    build.rs: file-level #![expect(...)].
  - Every #[expect] has a specific reason identifying the sync context.

Annotations added in ~90 files across the workspace. Notable narrow
placements: fabro-server server.rs current_server_target,
build_disk_usage_response, create_test_app_state_with_session_key;
fabro-server install.rs read_to_string rollback snapshot;
fabro-sandbox local.rs list_recursive; fabro-agent cli.rs FOLLOW-UP on
the JSON-stdout writer; fabro-llm providers/common.rs FOLLOW-UP for
load_file_as_base64 (7 translator call sites; revisit if file:// URL
usage grows).

build.rs blanket allows: fabro-api/build.rs, fabro-util/build.rs.

Pre-existing unrelated nightly-clippy warnings fixed under scope:
fabro-sandbox sandbox_spec.rs (unused_imports, unused_async),
reconnect.rs (unused_variables, unused_async).

Verified: cargo +nightly-2026-04-14 clippy --workspace --all-targets
-- -D warnings passes; fmt clean; 4129/4131 tests pass (two known
flakes under parallel nextest load, both pass individually and are
unrelated to this change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-19 17:22:21 -04:00
parent 9d1c0d98c7
commit 19939c5f07
No known key found for this signature in database
91 changed files with 465 additions and 13 deletions

View file

@ -8,6 +8,16 @@ disallowed-methods = [
{ path = "std::io::stdin", reason = "Returns a blocking handle; prefer tokio::io::stdin on Tokio paths. Document intentional sync stdin with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::io::stdout", reason = "Returns a blocking handle; prefer tokio::io::stdout on Tokio paths. Document intentional sync stdout with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::io::stderr", reason = "Returns a blocking handle; prefer tokio::io::stderr on Tokio paths. Document intentional sync stderr with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::fs::read", reason = "Blocking disk read; prefer tokio::fs::read on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::fs::read_to_string", reason = "Blocking disk read; prefer tokio::fs::read_to_string on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::fs::write", reason = "Blocking disk write; prefer tokio::fs::write on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::fs::read_dir", reason = "Blocking directory enumeration; prefer tokio::fs::read_dir on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::fs::copy", reason = "Blocking disk copy; prefer tokio::fs::copy on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::fs::canonicalize", reason = "Blocking path resolution; prefer tokio::fs::canonicalize on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::fs::File::open", reason = "Blocking open; prefer tokio::fs::File::open on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::fs::File::create", reason = "Blocking open; prefer tokio::fs::File::create on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::fs::File::create_new", reason = "Blocking open; prefer tokio::fs::File::create_new on Tokio paths. Document intentional sync I/O with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::fs::OpenOptions::open", reason = "Blocking open; prefer tokio::fs::OpenOptions::open on Tokio paths. OS file-lock semantics may require spawn_blocking instead. Document intentional sync I/O with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "reqwest::Client::new", reason = "Use fabro_http::http_client() or fabro_http::test_http_client()", allow-invalid = true },
{ path = "reqwest::Client::builder", reason = "Use fabro_http::HttpClientBuilder::new()", allow-invalid = true },
{ path = "reqwest::blocking::Client::new", reason = "Use fabro_http::blocking_http_client() or fabro_http::blocking_test_http_client()", allow-invalid = true },

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "agent parity test harness: sync std::fs for staging fixture trees and reading captured outputs"
)]
use std::collections::HashMap;
use std::fmt::Write as _;
use std::path::Path;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "build script: runs at compile time outside any runtime"
)]
use std::path::{Path, PathBuf};
use std::{env, fs};

View file

@ -586,6 +586,11 @@ mod tests {
#[cfg(unix)]
#[tokio::test]
#[expect(
clippy::disallowed_methods,
reason = "integration-style test: writes and reads a fake codex script via sync std::fs to \
verify the login_command string passes stdin correctly"
)]
async fn openai_api_key_cli_login_command_executes_codex_from_local_bin() {
let dir = tempfile::tempdir().unwrap();
let local_bin = dir.path().join(".local/bin");

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "sync git2 operations dominate this module; std::fs usage is part of the same blocking path and not on a Tokio hot path"
)]
use std::collections::BTreeMap;
use std::path::Path;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI `artifact cp` command: sync file I/O in command handler; not on a Tokio hot path"
)]
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI `doctor` command: sync directory scan in command handler"
)]
use std::path::{Path, PathBuf};
use anyhow::Result;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI `install` command: sync file I/O in install command handler; not on a Tokio hot path"
)]
use std::future::Future;
use std::net::SocketAddr;
use std::path::Path;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI server record helpers: sync read/write of local server record file"
)]
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI `server start` command: sync config/record file I/O in startup command; acquire_lock uses spawn_blocking"
)]
use std::path::{Path, PathBuf};
use std::time::Duration;
@ -14,6 +19,7 @@ use fabro_util::terminal::Styles;
use fabro_util::{Home, dev_token, session_secret};
use tokio::net::{TcpStream, UnixStream};
use tokio::process::Command as TokioCommand;
use tokio::task::spawn_blocking;
use tokio::time;
use super::record;
@ -514,7 +520,12 @@ async fn acquire_lock(storage_dir: &Path) -> Result<std::fs::File> {
.with_context(|| format!("creating server lock directory {}", parent.display()))?;
}
let lock_path_for_open = lock_path.clone();
let lock_file = tokio::task::spawn_blocking(move || {
#[expect(
clippy::disallowed_methods,
reason = "OpenOptions::open inside spawn_blocking; file-lock semantics need a real \
std::fs::File handle for fabro_proc::try_flock_exclusive"
)]
let lock_file = spawn_blocking(move || {
std::fs::OpenOptions::new()
.create(true)
.write(true)

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI `store dump` command: sync file I/O for dump outputs"
)]
use std::io::ErrorKind;
use std::path::Path;

View file

@ -2,6 +2,10 @@
clippy::disallowed_types,
reason = "sync CLI `uninstall` command: blocking std::io::Write is the intended output mechanism"
)]
#![expect(
clippy::disallowed_methods,
reason = "CLI `uninstall` command: sync file I/O in command handler"
)]
use std::fs;
use std::io::Write;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI `workflow create` command: sync file I/O creating workflow scaffolding"
)]
use std::path::Path;
use anyhow::{Context, Result, bail};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI logging setup: sync directory scan during startup"
)]
use std::path::Path;
use anyhow::{Context, Result};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI manifest builder: sync file I/O building install manifests"
)]
use std::collections::{HashMap, HashSet};
use std::path::{Component, Path, PathBuf};

View file

@ -1138,6 +1138,10 @@ fn non_zero_u64_from_usize(value: usize) -> Option<NonZeroU64> {
}
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "server-client tests stage local dev-token fixtures with sync std::fs::write"
)]
mod tests {
use super::*;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI user config: sync file I/O loading user config"
)]
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use std::path::PathBuf;
use fabro_config::parse_settings_layer;

View file

@ -1,4 +1,8 @@
#![allow(clippy::absolute_paths)]
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use std::process::Output;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_config::{Storage, envfile};
use fabro_test::{fabro_snapshot, test_context};
use fabro_vault::{SecretType, Vault};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_test::{fabro_snapshot, test_context};
use httpmock::MockServer;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_test::{fabro_snapshot, test_context};
use insta::assert_snapshot;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_test::{fabro_snapshot, test_context};
use super::support::{git_stdout, output_stderr, setup_git_backed_changed_run};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_auth::{AuthCredential, AuthDetails};
use fabro_config::Storage;
use fabro_model::Provider;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_test::{fabro_snapshot, test_context};
use super::support::{read_text, setup_created_dry_run, setup_local_sandbox_run, text_tree};

View file

@ -3,6 +3,10 @@
reason = "integration test: occupies a fixed TCP port via sync std::net::TcpListener to \
verify the server-start fallback path when the default port is unavailable"
)]
#![expect(
clippy::disallowed_methods,
reason = "integration test stages server-start fixtures with sync std::fs::write"
)]
use std::process::Stdio;
use std::sync::{Arc, Barrier};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use std::fs;
use std::time::Duration;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_test::{fabro_snapshot, test_context};
use serde_json::Value;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use std::fs;
use fabro_test::{fabro_snapshot, test_context};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use assert_cmd::Command;
use fabro_test::{TestContext, fabro_snapshot, test_context};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_test::{fabro_snapshot, test_context};
use insta::assert_snapshot;
use serde_json::Value;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use std::time::Duration;
use fabro_test::test_context;

View file

@ -1,4 +1,8 @@
#![allow(clippy::absolute_paths)]
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_test::test_context;
use serde_json::Value;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_test::test_context;
use super::{

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_test::test_context;
use super::{

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use fabro_test::test_context;
use super::{

View file

@ -3,6 +3,10 @@
clippy::needless_borrow,
clippy::needless_borrows_for_generic_args
)]
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use std::process::Output;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "sync env-file load/save used at CLI and server startup; not on a Tokio path"
)]
use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "sync config loading utilities used at startup; not on a Tokio path"
)]
extern crate self as fabro_config;
mod defaults;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "sync config file load used at startup; not on a Tokio path"
)]
use std::path::{Path, PathBuf};
use fabro_types::settings::run::RunGoalLayer;

View file

@ -4,6 +4,11 @@
//! tree in `fabro_types::settings::v2`. This module keeps the workflow
//! discovery helpers and re-exports resolved project settings.
#![expect(
clippy::disallowed_methods,
reason = "sync project-level config discovery and workflow listing; not on a Tokio path"
)]
use std::fmt::Write;
use std::path::{Component, Path, PathBuf};

View file

@ -5,6 +5,11 @@
//! that used to be re-exported from here live under
//! `fabro_types::settings::run` now.
#![expect(
clippy::disallowed_methods,
reason = "sync run-config loading helpers; not on a Tokio path"
)]
use std::path::{Path, PathBuf};
use fabro_types::settings::SettingsLayer;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "sync parse of docker-compose.yml files during devcontainer resolution; not on a Tokio path"
)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};

View file

@ -731,6 +731,10 @@ mod tests {
}
#[tokio::test]
#[expect(
clippy::disallowed_methods,
reason = "test fixture setup uses sync std::fs::write to create a fake feature directory"
)]
async fn fetch_feature_local_integration() {
let tmp_src = tempfile::tempdir().unwrap();
let feature_dir = tmp_src.path().join("my-feature");

View file

@ -161,6 +161,12 @@ pub struct DevcontainerResolver;
impl DevcontainerResolver {
/// path: repo root (or explicit .devcontainer/ path)
#[expect(
clippy::disallowed_methods,
reason = "FOLLOW-UP: DevcontainerResolver::resolve does sync std::fs::read_to_string / \
read_dir across several devcontainer.json lookups. One-shot per workflow run \
(not per-request); acceptable today but should migrate to tokio::fs."
)]
pub async fn resolve(path: &Path) -> Result<DevcontainerSpec> {
let (json_path, devcontainer) = Self::find_and_parse(path)?;
let repo_root = Self::repo_root_from_json_path(&json_path, path);
@ -409,6 +415,11 @@ impl DevcontainerResolver {
})
}
#[expect(
clippy::disallowed_methods,
reason = "FOLLOW-UP: sync std::fs helpers for devcontainer.json lookup; called once at \
workflow startup via resolve(). Should migrate to tokio::fs with resolve()."
)]
fn find_and_parse(path: &Path) -> Result<(PathBuf, DevcontainerJson)> {
// Check standard locations
let candidates = [

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "fabro-install: sync CLI install/uninstall bookkeeping; not on a Tokio hot path"
)]
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};

View file

@ -51,6 +51,10 @@ impl RecordingInterviewer {
///
/// # Errors
/// Returns an error if serialization or file writing fails.
#[expect(
clippy::disallowed_methods,
reason = "sync helper for test-mode interview recording storage; not on a Tokio path"
)]
pub fn save_to_file(&self, path: &Path) -> std::io::Result<()> {
let json = self.to_json()?;
std::fs::write(path, json).map_err(|err| {
@ -66,6 +70,10 @@ impl RecordingInterviewer {
///
/// # Errors
/// Returns an error if file reading or deserialization fails.
#[expect(
clippy::disallowed_methods,
reason = "sync helper for test-mode interview recording storage; not on a Tokio path"
)]
pub fn load_from_file(path: &Path) -> std::io::Result<Vec<(Question, Answer)>> {
let json = std::fs::read_to_string(path).map_err(|err| {
std::io::Error::new(

View file

@ -92,6 +92,13 @@ pub fn mime_from_extension(path: &str) -> &str {
///
/// # Errors
/// Returns an error if the file cannot be read.
#[expect(
clippy::disallowed_methods,
reason = "FOLLOW-UP: sync std::fs::read for file:// attachments, invoked from sync \
translators (translate_input/translate_messages) across all providers. Pre-existing; \
7 call sites in sync translators would need restructuring to wrap in spawn_blocking \
at each async chokepoint. file:// URLs are rare in practice; revisit if usage grows."
)]
pub fn load_file_as_base64(path: &str) -> Result<(String, String), std::io::Error> {
let expanded = path.strip_prefix("~/").map_or_else(
|| path.to_string(),

View file

@ -32,6 +32,10 @@ pub fn flock_unlock(file: &File) -> io::Result<()> {
}
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "tests exercise sync flock semantics on real temp files; uses std::fs::File directly"
)]
mod tests {
use std::fs::File;

View file

@ -4,6 +4,7 @@ use std::time::Instant;
use async_trait::async_trait;
use tokio::io::AsyncReadExt;
use tokio::process::{Child, Command};
use tokio::task::spawn_blocking;
use tokio::{fs, time};
use tokio_util::sync::CancellationToken;
@ -156,6 +157,10 @@ impl Sandbox for LocalSandbox {
path: &str,
depth: Option<usize>,
) -> Result<Vec<DirEntry>, String> {
#[expect(
clippy::disallowed_methods,
reason = "sync recursive read_dir; caller wraps invocation in tokio::task::spawn_blocking"
)]
fn list_recursive(
base: &std::path::Path,
prefix: &str,
@ -197,7 +202,7 @@ impl Sandbox for LocalSandbox {
let full_path = self.resolve_path(path);
let max_depth = depth.unwrap_or(1);
tokio::task::spawn_blocking(move || {
spawn_blocking(move || {
let mut entries = Vec::new();
list_recursive(&full_path, "", 0, max_depth, &mut entries)?;
Ok(entries)
@ -537,6 +542,10 @@ async fn sigterm_then_kill(child: &mut Child) {
}
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "sandbox tests stage fixtures with sync std::fs writes/reads"
)]
mod tests {
use std::path::PathBuf;

View file

@ -14,6 +14,7 @@ use crate::sandbox_record::SandboxRecord;
///
/// `daytona_api_key` is forwarded to the Daytona SDK when the provider is
/// `"daytona"`. Pass `None` to fall back to the `DAYTONA_API_KEY` env var.
#[allow(clippy::unused_async, unused_variables)]
pub async fn reconnect(
record: &SandboxRecord,
daytona_api_key: Option<String>,

View file

@ -5,6 +5,7 @@ use std::sync::Arc;
use anyhow::anyhow;
#[cfg(feature = "daytona")]
use fabro_github::GitHubCredentials;
#[allow(unused_imports)]
use fabro_types::RunId;
use crate::config::WorktreeMode;
@ -142,6 +143,7 @@ impl SandboxSpec {
}
}
#[allow(clippy::unused_async)]
pub async fn build(
&self,
event_callback: Option<SandboxEventCallback>,

View file

@ -358,6 +358,10 @@ impl Sandbox for WorktreeSandbox {
// ---------------------------------------------------------------------------
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "worktree tests stage fixtures with sync std::fs writes in temp dirs"
)]
mod tests {
use std::sync::Mutex;

View file

@ -891,6 +891,11 @@ async fn post_install_finish(
("FABRO_DEV_TOKEN".to_string(), dev_token.clone()),
]);
#[expect(
clippy::disallowed_methods,
reason = "install-finish handler: reads current settings file once to produce a rollback \
snapshot before writing the new settings; one-shot per install-finish request"
)]
let previous_settings = std::fs::read_to_string(state.config_path.as_ref()).ok();
if let Err(err) = persist_install_outputs_direct(

View file

@ -12,6 +12,7 @@ use fabro_types::settings::server::{
};
use ipnet::IpNet;
use serde::{Deserialize, Serialize};
use tokio::fs;
use tracing::warn;
use crate::ApiError;
@ -130,7 +131,7 @@ impl GitHubMetaResolver {
}
async fn load_cache(&self) -> Result<Option<GitHubMetaCache>> {
match tokio::fs::read(&self.cache_path).await {
match fs::read(&self.cache_path).await {
Ok(contents) => match serde_json::from_slice(&contents) {
Ok(cache) => Ok(Some(cache)),
Err(error) => {
@ -151,13 +152,13 @@ impl GitHubMetaResolver {
async fn store_cache(&self, cache: &GitHubMetaCache) -> Result<()> {
if let Some(parent) = self.cache_path.parent() {
tokio::fs::create_dir_all(parent)
fs::create_dir_all(parent)
.await
.with_context(|| format!("creating {}", parent.display()))?;
}
let contents = serde_json::to_vec(cache).context("serializing GitHub meta cache")?;
tokio::fs::write(&self.cache_path, contents)
fs::write(&self.cache_path, contents)
.await
.with_context(|| format!("writing {}", self.cache_path.display()))?;
Ok(())
@ -322,6 +323,10 @@ pub fn github_meta_cache_path(cache_dir: &Path) -> PathBuf {
}
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "tests stage IP allowlist fixtures with sync std::fs::write"
)]
mod tests {
use std::net::Ipv4Addr;

View file

@ -1420,6 +1420,10 @@ struct PrunePlan {
total_size_bytes: u64,
}
#[expect(
clippy::disallowed_methods,
reason = "sync helper invoked from async handler via spawn_blocking (see callers at :1301 / :1341)"
)]
fn build_disk_usage_response(
summaries: &[fabro_store::RunSummary],
storage_dir: &std::path::Path,
@ -2392,6 +2396,10 @@ pub fn create_app_state_with_env_lookup(
}
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "test helper writes a fixture server.env with sync std::fs::write"
)]
pub(crate) fn create_test_app_state_with_session_key(
settings: SettingsLayer,
session_secret: Option<&str>,
@ -3535,6 +3543,11 @@ struct WorkerServerRecord {
fn current_server_target(storage_dir: &std::path::Path) -> anyhow::Result<String> {
let record_path = Storage::new(storage_dir).server_state().record_path();
#[expect(
clippy::disallowed_methods,
reason = "sync helper invoked from worker_command (sync) via spawn_blocking at the async \
boundary in execute_run_subprocess; see commit 9d1c0d98c"
)]
let content = std::fs::read_to_string(&record_path)
.map_err(|err| anyhow::anyhow!("failed to read {}: {err}", record_path.display()))?;
let record: WorkerServerRecord = serde_json::from_str(&content).map_err(|err| {
@ -4462,7 +4475,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
let state_for_build = Arc::clone(&state);
let run_dir_for_build = run_dir.clone();
let build_cmd_result = tokio::task::spawn_blocking(move || {
let build_cmd_result = spawn_blocking(move || {
worker_command(
state_for_build.as_ref(),
run_id,
@ -6894,6 +6907,10 @@ async fn get_graph(
}
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "server unit tests stage fixtures with sync std::fs writes"
)]
mod tests {
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

View file

@ -4,6 +4,7 @@ use std::sync::OnceLock;
use axum::body::Body;
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use tokio::fs;
const INSTALL_MODE_MARKER: &str = "__FABRO_MODE__ = \"install\"";
@ -143,7 +144,7 @@ async fn read_disk_asset(path: &str) -> Option<Vec<u8>> {
async fn read_disk_asset_from_root(root: &Path, path: &str) -> Option<Vec<u8>> {
let candidate = root.join(path);
if candidate.is_file() {
tokio::fs::read(candidate).await.ok()
fs::read(candidate).await.ok()
} else {
None
}
@ -197,6 +198,10 @@ fn is_source_map(path: &str) -> bool {
}
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "tests stage static asset fixtures with sync std::fs::write"
)]
mod tests {
use axum::http::{HeaderMap, HeaderValue, header};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use std::path::PathBuf;
fn read_doc(relative_path: &str) -> String {

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use std::path::PathBuf;
use std::time::Duration;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;

View file

@ -6,6 +6,10 @@
clippy::manual_assert,
clippy::manual_let_else
)]
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use axum::body::Body;
use axum::http::{Method, Request, StatusCode};

View file

@ -29,6 +29,10 @@ pub fn get(path: &str) -> Option<AssetBytes> {
}
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "test walks the assets/ directory with sync std::fs::read_dir to enforce a build invariant"
)]
mod tests {
use std::path::{Path, PathBuf};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "sync read/write of the anonymous-id cache file; not on a Tokio path"
)]
use std::fs;
use std::path::Path;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "sync read of the panic log at telemetry startup; not on a Tokio path"
)]
use std::panic::PanicHookInfo;
use std::path::Path;

View file

@ -123,6 +123,11 @@ pub async fn upload(path: &Path) -> anyhow::Result<()> {
let write_key = SEGMENT_WRITE_KEY
.ok_or_else(|| anyhow::anyhow!("SEGMENT_WRITE_KEY not set at compile time"))?;
#[expect(
clippy::disallowed_methods,
reason = "telemetry uploader invoked via detached subprocess (__send_analytics); runs \
outside the main Tokio runtime as a standalone process, so sync read is fine"
)]
let content = std::fs::read_to_string(path)
.with_context(|| format!("read telemetry batch {}", path.display()))?;
let Some(payload) = build_segment_batch(&content) else {

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "sync pre-fork filesystem interaction; the whole module runs before fork/exec"
)]
/// Spawn a fully detached subprocess that survives parent exit and terminal
/// close.
///

View file

@ -1,3 +1,10 @@
#![expect(
clippy::disallowed_methods,
reason = "fabro-test: shared test infrastructure; sync std::fs throughout is intentional for \
test fixtures, snapshots, and scratch directories. Tokio-path code under test sits \
in other crates."
)]
use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "build script: runs at compile time outside any runtime"
)]
use std::fmt::Write;
use std::path::Path;
use std::{env, fs};

View file

@ -1,3 +1,9 @@
#![expect(
clippy::disallowed_methods,
reason = "sync atomic read/write of the local dev token file; not on a Tokio hot path. \
OpenOptions::open is used for setting 0o600 mode on unix"
)]
use std::fs;
#[expect(
clippy::disallowed_types,

View file

@ -3,6 +3,10 @@
reason = "file-backed tracing sink: sync BufWriter<File> is intentional; writes happen on a \
dedicated per-event guard and are not in an async hot path"
)]
#![expect(
clippy::disallowed_methods,
reason = "sync File::create and read_to_string for the on-disk run-log file; not on Tokio path"
)]
use std::io::{self, BufWriter, Write};
use std::path::Path;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests that exercise sync dev-token file operations"
)]
use std::fs;
use fabro_util::Home;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "fabro-vault: sync secret-file storage; not used on a Tokio hot path"
)]
use std::collections::HashMap;
use std::path::{Component, Path, PathBuf};
use std::{fmt, io};

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "FOLLOW-UP: mixed async/sync workflow artifact lifecycle. Sync std::fs::write remains at per-stage persistence points; the Tokio-path hot reads were migrated to tokio::fs in commit 9d1c0d98c. Remaining writes should follow."
)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
@ -9,6 +14,7 @@ use fabro_types::{
};
use futures::future::BoxFuture;
use serde_json::Value;
use tokio::fs;
use crate::context::{self, Context};
use crate::error::{Error, Result};
@ -174,7 +180,7 @@ pub async fn sync_artifacts_to_env(
}
}
let content = tokio::fs::read_to_string(&local_path).await.map_err(|e| {
let content = fs::read_to_string(&local_path).await.map_err(|e| {
Error::engine(format!("failed to read local artifact {local_path}: {e}"))
})?;
@ -325,7 +331,7 @@ async fn resolve_explicit_file_ref(value: &str, env: &dyn Sandbox) -> Result<Str
return Ok(value.to_string());
}
let content = tokio::fs::read_to_string(local_path)
let content = fs::read_to_string(local_path)
.await
.map_err(|e| Error::engine(format!("failed to read local artifact {local_path}: {e}")))?;
let filename = Path::new(local_path)

View file

@ -4,6 +4,7 @@ use fabro_agent::Sandbox;
use fabro_sandbox::shell_quote;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use tokio::fs;
use tracing::{debug, warn};
/// A file discovered by the find command.
@ -264,7 +265,7 @@ async fn compute_artifact_info(
let mime = mime_guess::from_path(relative_path)
.first_or_octet_stream()
.to_string();
let data = tokio::fs::read(local_path)
let data = fs::read(local_path)
.await
.map_err(|e| format!("failed to read {}: {e}", local_path.display()))?;
let bytes = u64::try_from(data.len()).unwrap_or(u64::MAX);
@ -353,6 +354,7 @@ pub async fn collect_artifacts(
}
#[cfg(test)]
#[expect(clippy::disallowed_methods, reason = "tests write fixtures to disk")]
mod tests {
use std::collections::HashMap;
@ -442,11 +444,11 @@ mod tests {
.get(remote_path)
.ok_or_else(|| format!("File not found: {remote_path}"))?;
if let Some(parent) = local_path.parent() {
tokio::fs::create_dir_all(parent)
fs::create_dir_all(parent)
.await
.map_err(|e| format!("Failed to create dirs: {e}"))?;
}
tokio::fs::write(local_path, content.as_bytes())
fs::write(local_path, content.as_bytes())
.await
.map_err(|e| format!("Failed to write: {e}"))?;
Ok(())

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "sync workflow file resolver invoked at stage setup; not on a Tokio hot path"
)]
use std::collections::HashMap;
use std::path::{Component, Path, PathBuf};

View file

@ -314,6 +314,10 @@ pub fn sanitize_ref_component(s: &str) -> String {
/// Filenames allowed in per-node directories on the shadow branch.
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "tests write git state fixtures to disk"
)]
mod tests {
use std::fs;
use std::sync::Arc;

View file

@ -395,6 +395,10 @@ impl Handler for AgentHandler {
}
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "tests persist per-iteration state fixtures"
)]
mod tests {
use std::sync::Arc;
use std::time::Duration;

View file

@ -348,6 +348,10 @@ impl Handler for SubWorkflowHandler {
}
#[cfg(test)]
#[expect(
clippy::disallowed_methods,
reason = "tests persist manager-loop state fixtures"
)]
mod tests {
use std::collections::HashMap;
use std::sync::Arc;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "FOLLOW-UP: per-run `workflow create` operation; writes .fabro/ scaffolding to disk via sync std::fs"
)]
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use std::sync::Arc;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "FOLLOW-UP: rebuild metadata uses sync std::fs::canonicalize during async checkpoint rebuild; per-run, not per-request"
)]
use std::collections::HashMap;
use std::fmt::Write;
use std::path::PathBuf;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "sync workflow operation loader; runs at workflow-load time"
)]
use std::path::{Path, PathBuf};
use std::sync::Arc;

View file

@ -53,6 +53,7 @@ pub(crate) async fn load_from_store(
}
#[cfg(test)]
#[expect(clippy::disallowed_methods, reason = "tests stage pipeline fixtures")]
mod tests {
use std::collections::HashMap;
use std::path::PathBuf;

View file

@ -53,6 +53,7 @@ pub fn transform(parsed: Parsed, options: &TransformOptions) -> Result<Transform
}
#[cfg(test)]
#[expect(clippy::disallowed_methods, reason = "tests stage pipeline fixtures")]
mod tests {
use std::collections::HashMap;
use std::path::Path;

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "FOLLOW-UP: run dump writes serialized run contents to disk; invoked from sync CLI command handlers"
)]
use std::collections::HashMap;
#[expect(
clippy::disallowed_types,

View file

@ -1,3 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "FOLLOW-UP: run lookup walks the runs directory; invoked from sync CLI and async server paths"
)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};

View file

@ -240,7 +240,7 @@ pub async fn git_replace_worktree(sandbox: &dyn Sandbox, path: &str, branch: &st
mod tests {
#![expect(
clippy::disallowed_methods,
reason = "These unit tests use the real git CLI to construct sandbox-git fixture repositories."
reason = "These unit tests use the real git CLI to construct sandbox-git fixture repositories and sync-write fixtures to disk."
)]
use super::*;

View file

@ -615,6 +615,7 @@ impl Transform for ImportTransform {
}
#[cfg(test)]
#[expect(clippy::disallowed_methods, reason = "tests stage transform fixtures")]
mod tests {
use std::path::Path;
use std::sync::Arc;

View file

@ -1,4 +1,8 @@
#![allow(clippy::absolute_paths)]
#![expect(
clippy::disallowed_methods,
reason = "workflow attractor compat test: reads fixture files with sync std::fs"
)]
use std::path::Path;

View file

@ -6,6 +6,10 @@
//! cp_integration -- --ignored`
#![allow(clippy::ignore_without_reason)]
#![expect(
clippy::disallowed_methods,
reason = "workflow cp integration test: stages sandbox fixtures with sync std::fs"
)]
use fabro_sandbox::SandboxRecord;
use fabro_sandbox::reconnect::reconnect;

View file

@ -170,6 +170,10 @@ pub struct FixtureWebhookOptions {
impl FixtureState {
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, String> {
let path = path.as_ref();
#[expect(
clippy::disallowed_methods,
reason = "twin test harness: sync fixture load from disk; not on a Tokio hot path"
)]
let contents = fs::read_to_string(path)
.map_err(|err| format!("failed to read fixture {}: {err}", path.display()))?;
serde_json::from_str(&contents)