You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Generalize the "internal bridge API + dylint ban on the third-party primitive" pattern that fbuild already uses for subprocess, paths, serial, and deploy. Per user directive: no grandfathering, no backwards compatibility concerns, no extant allowlists carried forward. Beta software — sweep everything.
A 5-agent audit on 2026-06-29 surveyed the workspace for every place that bypasses an existing bridge or would benefit from a new one. All work is rolled into this one issue (was split across #844 + #845-#848 briefly).
Existing lints (do not re-do)
Internal API
Dylint
Banned primitive
Origin
fbuild_core::subprocess::run_command
ban_raw_subprocess
std::process::Command::{spawn,output,status} + tokio variant
Every box below must be checked before this issue closes.
Bridge pair 1 — HTTP client
Hoist fbuild_packages::http::client() to fbuild_core::http::client(), with client_with_timeout(Duration) and blocking_client(Duration) (latter for the port_scan OS-thread case).
Ship ban_bare_reqwest lint with zero allowlist (forbids reqwest::get, reqwest::blocking::get, reqwest::Client::new, reqwest::ClientBuilder outside the bridge).
Bridge pair 2 — File I/O in async
Add fbuild_core::fs::* curated re-export of tokio::fs::*.
Migrate all std::fs::* calls reachable from async fn (audit found 18 in 7 files as direct tokio::fs imports; per "no grandfathering" every std::fs in async context migrates too).
Ship ban_std_fs_in_async lint (detects std::fs::* inside async fn or tokio::spawn closures; exempts spawn_blocking closures). Zero allowlist.
Ship ban_tokio_fs_direct_import lint forcing imports through fbuild_core::fs. Zero allowlist.
Bridge pair 3 — Sleep
Add fbuild_core::time::sleep (re-export of tokio::time::sleep).
Migrate 67 std::thread::sleep sites to fbuild_core::time::sleep or tokio::time::sleep.
Ship ban_std_thread_sleep lint with zero allowlist (the user directive overrides the prior thought of exempting test/sync helpers).
Migrate 54 std::sync::mpsc sites + 23 direct tokio::sync::mpsc imports.
Ship ban_std_mpsc_in_async_reachable lint and ban_tokio_mpsc_direct_import lint. Zero allowlist.
Bridge pair 5 — Path canonicalize
Add fbuild_core::path::canonicalize_existing(path) that calls std::fs::canonicalize + strips Windows \\?\ UNC prefix + returns NormalizedPath.
Migrate 23 std::fs::canonicalize sites (highest concentration: crates/fbuild-header-scan/src/walker.rs with 7).
Ship ban_std_fs_canonicalize lint. Zero allowlist.
Bridge pair 6 — Atomic state-file writes
Add fbuild_core::fs::write_atomic(path, content) — writes to <path>.tmp.<pid>.<nonce>, File::sync_all(), then atomic rename (MoveFileExW with MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH on Windows).
Migrate 248 println! / eprintln! calls in crates/fbuild-cli/src/**.
Migrate 12+ debug println!/eprintln! calls in crates/fbuild-build/src/**/*linker*.rs and per-platform orchestrators to tracing::debug!(target = "fbuild_build::linker", ...).
Ship ban_print_in_production lint scoped to crates/fbuild-cli/src/** and crates/fbuild-build/src/**, allowlist = [crates/fbuild-cli/src/output.rs] (the bridge module itself).
--color={auto,always,never}, --quiet, --verbose flags consistently routed through tracing's level filter.
Smaller tightenings
run_command(None) audit — 8 sites currently pass literal None for the timeout argument:
No allowlists this round. Per user directive, every new lint ships with zero allowlist entries — code that doesn't comply gets fixed first.
Bridge has strictly less surface than the primitive. If you find yourself re-exporting every flag, kill the wrapper.
Lint + API land in the same PR. Don't ship a ban without a sanctioned replacement.
Cost ceiling
fbuild is at 11 lints today. After this checklist completes, it will be at ~20-22. That's the practical ceiling for contributor cognitive load. Anything beyond this should be code review / CodeRabbit rules, not dylint.
Goal
Generalize the "internal bridge API + dylint ban on the third-party primitive" pattern that fbuild already uses for subprocess, paths, serial, and deploy. Per user directive: no grandfathering, no backwards compatibility concerns, no extant allowlists carried forward. Beta software — sweep everything.
A 5-agent audit on 2026-06-29 surveyed the workspace for every place that bypasses an existing bridge or would benefit from a new one. All work is rolled into this one issue (was split across #844 + #845-#848 briefly).
Existing lints (do not re-do)
fbuild_core::subprocess::run_commandban_raw_subprocessstd::process::Command::{spawn,output,status}+ tokio variantfbuild_core::path::NormalizedPathban_std_pathbufstd::path::PathBuffbuild_serial::SharedSerialManagerban_direct_serialportuse serialport::outside fbuild-serialfbuild_deploy::Deployertraitban_deploy_tool_direct_invocationCommand::new("esptool"|"avrdude"|...)ban_file_based_locksOpenOptions::create_new(true),fs2::FileExt,flock(ban_unrooted_tempdirtempfile::TempDir::new(),tempfile::tempdir()ban_std_sync_mutex_in_asyncstd::sync::Mutexin daemon/serial scopeban_process_exit_outside_mainstd::process::exitoutsidemain.rsban_unwrap_in_daemon_handlers.unwrap()insidecrates/fbuild-daemon/src/handlers/cli_no_build_deploy_direct_usefbuild_build::*/fbuild_deploy::*fromcrates/fbuild-cli/src/require_multi_thread_flavor_when_spawning#[tokio::test]w/oflavor = "multi_thread"on tests that spawnClosing checklist
Every box below must be checked before this issue closes.
Bridge pair 1 — HTTP client
fbuild_packages::http::client()tofbuild_core::http::client(), withclient_with_timeout(Duration)andblocking_client(Duration)(latter for theport_scanOS-thread case).reqwest::Client::new()direct-construct sites:crates/fbuild-python/src/daemon.rs:150,184,238,300,314crates/fbuild-python/src/async_serial_monitor.rs:348crates/fbuild-python/src/outcome.rs:151crates/fbuild-cli/src/daemon_client.rs:209-212crates/fbuild-cli/src/cli/port_scan.rs:129crates/fbuild-daemon/tests/{test_emu_endpoint.rs:75, build_streaming.rs:59}ban_bare_reqwestlint with zero allowlist (forbidsreqwest::get,reqwest::blocking::get,reqwest::Client::new,reqwest::ClientBuilderoutside the bridge).Bridge pair 2 — File I/O in async
fbuild_core::fs::*curated re-export oftokio::fs::*.std::fs::*calls reachable fromasync fn(audit found 18 in 7 files as directtokio::fsimports; per "no grandfathering" every std::fs in async context migrates too).ban_std_fs_in_asynclint (detectsstd::fs::*insideasync fnortokio::spawnclosures; exemptsspawn_blockingclosures). Zero allowlist.ban_tokio_fs_direct_importlint forcing imports throughfbuild_core::fs. Zero allowlist.Bridge pair 3 — Sleep
fbuild_core::time::sleep(re-export oftokio::time::sleep).std::thread::sleepsites tofbuild_core::time::sleeportokio::time::sleep.ban_std_thread_sleeplint with zero allowlist (the user directive overrides the prior thought of exempting test/sync helpers).Bridge pair 4 — Channels
fbuild_core::channel::{bounded, unbounded}wrappingtokio::sync::mpsc.std::sync::mpscsites + 23 directtokio::sync::mpscimports.ban_std_mpsc_in_async_reachablelint andban_tokio_mpsc_direct_importlint. Zero allowlist.Bridge pair 5 — Path canonicalize
fbuild_core::path::canonicalize_existing(path)that callsstd::fs::canonicalize+ strips Windows\\?\UNC prefix + returnsNormalizedPath.std::fs::canonicalizesites (highest concentration:crates/fbuild-header-scan/src/walker.rswith 7).ban_std_fs_canonicalizelint. Zero allowlist.Bridge pair 6 — Atomic state-file writes
fbuild_core::fs::write_atomic(path, content)— writes to<path>.tmp.<pid>.<nonce>,File::sync_all(), then atomic rename (MoveFileExWwithMOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGHon Windows).crates/fbuild-build/src/build_info.rs:291(build fingerprint JSON)crates/fbuild-packages/src/toolchain/esp32_metadata.rs:236,247,273,283(framework discovery cache)crates/fbuild-cli/src/cli/symbols_cmd.rs:180,clang_tools.rs:206,clangd_config.rs:87crates/fbuild-daemon/src/status_manager.rs:210-225(already does the pattern manually) to call the shared helper.write_atomic. (Not lint-feasible — false-positive heavy.)Bridge pair 7 — Named
Durationconstantsfbuild_core::time::{SHORT_HTTP_TIMEOUT, MEDIUM_HTTP_TIMEOUT, LONG_HTTP_TIMEOUT, POST_DEPLOY_RECOVERY_DEADLINE, DAEMON_LONG_OP_TIMEOUT, POLL_50MS, POLL_100MS, POLL_200MS, REAL_BUILD_TIMEOUT}etc.Duration::from_*literals; the named-constant set covers the top 80%. Migrate the top-10 most-used patterns.Lint 8 —
Runtime::new()outside entry pointsban_runtime_new_outside_main— only allowed inmain.rs,src/bin/*.rs,#[cfg(test)]modules.crates/fbuild-packages/src/library/library_manager.rs:314crates/fbuild-packages/src/lnk/resolver.rs:120crates/fbuild-packages/src/toolchain/avr.rs:351(currently.unwrap()— fix the panic too)crates/fbuild-packages/src/toolchain/esp32_metadata.rs:87crates/fbuild-python/src/lib.rs(3 sites)crates/fbuild-python/src/serial_monitor.rs:199Lint 9 — Poison-panic ban
ban_poison_paniclint flaggingstd::sync::Mutex::lock().unwrap()/expect(_)andstd::sync::RwLock::{read,write}().unwrap()/expect(_)workspace-wide.std::sync::Mutexworkspace-wide).crates/fbuild-daemon/src/broker/{backend.rs:149, service.rs:401}crates/fbuild-daemon/src/context.rs:615crates/fbuild-daemon/src/device_manager.rs— multiple sitesLint 10 — Tempfile root migration (drive existing allowlist to zero)
fbuild_paths::dev_or_prod_temp_root()returning~/.fbuild/{dev|prod}/tmp/<subdir>/.dylints/ban_unrooted_tempdir/src/allowlist.txt:crates/fbuild-packages/src/extractor.rs(HIGH: per-package-install)crates/fbuild-packages/src/library/esp32_framework/libs.rs(HIGH: per-env install)crates/fbuild-packages/src/disk_cache/{gc,mod}.rs(HIGH: cache cleanup)crates/fbuild-build/src/linker.rs(HIGH: per-build)crates/fbuild-build/src/framework_core_cache.rs(HIGH: framework hydration)dylints/ban_unrooted_tempdir/src/allowlist.txt(or leave header-only) after all sites migrate.Lint 11 — Extended unwrap discipline
ban_unwrap_in_daemon_handlersscope fromcrates/fbuild-daemon/src/handlers/**to:crates/fbuild-daemon/src/**.crates/fbuild-cli/src/cli/**..unwrap()calls across the workspace. Per-crate sweep order:fbuild-paths/fbuild-core/fbuild-config(smallest leaves)fbuild-packages/disk_cache/fbuild-packages/library/(5 high-count files: 46, 43, 42, 38, 36)fbuild-build/build_info.rs(50),build_fingerprint/(101),framework_libs.rs(76),pipeline/library.rs(39)fbuild-daemon/models.rs(51),device_manager.rs,broker/fbuild-cli/src/cli/Lint 12 — CLI output discipline
crates/fbuild-cli/src/output.rscurated API:progress(),result(),warn(),error(),debug()—tracing-backed.println!/eprintln!calls incrates/fbuild-cli/src/**.println!/eprintln!calls incrates/fbuild-build/src/**/*linker*.rsand per-platform orchestrators totracing::debug!(target = "fbuild_build::linker", ...).ban_print_in_productionlint scoped tocrates/fbuild-cli/src/**andcrates/fbuild-build/src/**, allowlist =[crates/fbuild-cli/src/output.rs](the bridge module itself).--color={auto,always,never},--quiet,--verboseflags consistently routed throughtracing's level filter.Smaller tightenings
run_command(None)audit — 8 sites currently pass literalNonefor the timeout argument:crates/fbuild-daemon/src/handlers/emulator/avr8js_npm.rs:14(node --versionprobe)crates/fbuild-daemon/src/handlers/emulator/runners.rs:250(simavr --helpprobe)crates/fbuild-packages/src/library/library_compiler.rs:373, 552crates/fbuild-core/src/subprocess.rsMigrate each to either explicit
Some(Duration::from_secs(N))orrun_command_no_timeout(...). Then add a lint to reject the literalNone.Audit findings — NOT worth a lint (confirmed clean or no policy concern)
These were surveyed but require no action:
TcpStream/TcpListener/UnixStream/interprocess::local_socket— all daemon-axum, broker IPC, or test.ToSocketAddrs/trust_dns/hickoryuse.crossbeam::channel— not used.std::thread::JoinHandle::join()— zero blocking joins.std::thread::spawndirect use — 3 sites, all intentional (broker IPC backend,port_scanreqwest isolation, test mock).walkdir/ignore/globwalk— minimal use (8 sites), no policy needs.espflashdirect use — correct (library API inspawn_blockingfromDeployer).uv— clean in production.cargo/rustc/gitdirect invocation — zero sites in production.duct,xshell,cmd_lib,os_pipe) — none in deps.panic!/todo!/unimplemented!in production — clean (1panic!in test).block_oninsideasync fn— none found.tokio::spawn(...).awaitanti-pattern — none found.unsafeblocks — 30 sites, all with// SAFETY:comments.Box::leak/mem::transmute— 7 sites, all intentional (containment harness).std::env::set_var/remove_varin production — 7 sites, all for child-process environ setup.tokio::sync::broadcast/tokio::sync::watch— per-feature, correct.Orderingchoice — 70 sites, all correct.Operating rules
Cost ceiling
fbuild is at 11 lints today. After this checklist completes, it will be at ~20-22. That's the practical ceiling for contributor cognitive load. Anything beyond this should be code review / CodeRabbit rules, not dylint.
References
ban_std_pathbufallowlist drive-down — proof the playbook works