Krolyxon
Commit 66d7a70

Branches

Refs →

budctl66d7a70

Drop cargo-deny and the network bans; rule 1 is about telemetry

Rule 1 was meant to keep out telemetry and cloud services, not every network
crate. Remove deny.toml, the clippy disallowed-types for TCP/UDP sockets, and
the guards.rs tests that searched for network code and checked the build
against the ban list. CLAUDE.md rule 1 now says: no telemetry or cloud,
network only when a feature truly needs it.
Author
krolyxon
Date
2026-10-28 10:40 +0530
Parent
1978b2f
Commit
66d7a708e1adc60701e9cf247c967459cc56f71e

+5 −197

7 files changed

  1. M CLAUDE.md +2 −6
  2. M clippy.toml +0 −7
  3. M crates/budctl-core/src/bluez.rs +1 −1
  4. M crates/budctl-core/src/lib.rs +1 −2
  5. M crates/budctl-core/tests/guards.rs +0 −105
  6. D deny.toml +0 −75
  7. M docs/protocol/transport.md +1 −1
M CLAUDE.md View
@@ -21,11 +21,7 @@ The first supported device is the **realme Buds T310**. More devices are added a
2121
2222 ## Hard rules
2323
24−1. **No telemetry; network only when a feature needs it.** The owner relaxed this rule on 2026-10-27: network access is fine when a feature truly needs it, but nothing may ever report on the user. No feature needs the network so far. The sockets budctl opens today:
25− - Bluetooth sockets: RFCOMM for the control link, L2CAP for SDP.
26− - The local system D-Bus Unix socket, used through `zbus` only to talk to BlueZ (`org.bluez`). budctl opens that socket itself at the fixed path, so an environment variable can never redirect it to TCP.
27− - `budctl-gui` only: the Wayland compositor's Unix socket. The GUI is built for Wayland only (no X11), so `DISPLAY` can never point it at TCP.
28− - HTTP clients (reqwest, hyper, ureq, surf, etc.) stay banned until a feature needs one. The bans live in `cargo deny` (`deny.toml`), clippy `disallowed-types` and `crates/budctl-core/tests/guards.rs`. Lifting one is fine then; say why in the commit.
24+1. **No telemetry or cloud.** Nothing may ever report on the user: no analytics, crash reports, usage data, update checks or accounts. Network access is fine when a feature truly needs it; none does so far.
2925 2. **Clean-room protocol work.** Protocol facts (UUIDs, channel numbers, packet framing, command IDs, init sequences) may come from public references, but no other project's source code is ever translated or copied. Write findings into `docs/protocol/<device>.md` in our own words, then implement from that spec.
3026 - Committed files do not name or cite those references. Each fact carries an evidence level instead (rule 3).
3127 - Only the owner can change this rule, by editing it here.
@@ -45,7 +41,7 @@ The first supported device is the **realme Buds T310**. More devices are added a
4541 - `zbus` (blocking API) only to list paired devices.
4642 - Supporting BLE GATT would need a new transport kind in core.
4743 - CLI: `clap` (derive). It is parsed with `try_parse`, so usage errors exit 1, not clap's default 2. `serde_json` provides `--json`.
48−- Tests: `cargo test --workspace`. Lints: `cargo clippy --workspace --all-targets -- -D warnings`. Dependency policy: `cargo deny check bans licenses sources`.
44+- Tests: `cargo test --workspace`. Lints: `cargo clippy --workspace --all-targets -- -D warnings`.
4945 - Keep dependencies minimal. Every new dependency needs a reason in the commit message. The current direct dependencies:
5046 - core: `libc`, `zbus`, `serde`
5147 - cli: `clap`, `serde_json`
M clippy.toml View
@@ -4,13 +4,6 @@ allow-expect-in-tests = true
44 allow-panic-in-tests = true
55 allow-indexing-slicing-in-tests = true
66
7−# CLAUDE.md hard rule 1: no network sockets, ever.
8−disallowed-types = [
9− { path = "std::net::TcpStream", reason = "budctl never opens network sockets (CLAUDE.md rule 1)" },
10− { path = "std::net::TcpListener", reason = "budctl never opens network sockets (CLAUDE.md rule 1)" },
11− { path = "std::net::UdpSocket", reason = "budctl never opens network sockets (CLAUDE.md rule 1)" },
12−]
13−
147 disallowed-methods = [
158 { path = "std::thread::sleep", reason = "wait with Link::pump_until, so the buds' requests are still answered" },
169 { path = "std::process::exit", reason = "return an error and let main turn it into an exit code" },
M crates/budctl-core/src/bluez.rs View
@@ -1,6 +1,6 @@
11 //! Paired devices, from BlueZ over the system D-Bus.
22 //!
3−//! The only use of D-Bus in budctl is listing paired devices (CLAUDE.md, hard rule 1). budctl
3+//! The only use of D-Bus in budctl is listing paired devices. budctl
44 //! connects to the system bus socket at its fixed path itself, so no environment variable can
55 //! point the connection anywhere else (such as a TCP address).
66
M crates/budctl-core/src/lib.rs View
@@ -3,8 +3,7 @@
33 //! A front end (CLI, GUI) calls [`scan`], picks a device with [`select`], opens a session with
44 //! [`connect`] and then reads or changes settings through [`devices::Session`].
55 //!
6−//! No network: the only sockets are Bluetooth sockets and the local system D-Bus (BlueZ).
7−//! See CLAUDE.md, hard rule 1.
6+//! No telemetry: the only sockets are Bluetooth sockets and the local system D-Bus (BlueZ).
87
98 pub mod bluez;
109 pub mod bt;
M crates/budctl-core/tests/guards.rs View
@@ -1,6 +1,5 @@
11 //! Checks for the CLAUDE.md hard rules that the compiler and clippy cannot make on their own.
22 //!
3−//! - Rule 1: no network code in our sources, and no banned crate (deny.toml) in a Linux build.
43 //! - Rule 3: every `Spec: <file>.md#<anchor>` comment points at an anchor that exists.
54 //!
65 //! The CLI's exit codes are checked in crates/budctl-cli/tests/cli.rs.
@@ -8,10 +7,8 @@
87 // Test-only file: helpers may panic (clippy.toml exempts only #[test] functions themselves).
98 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
109
11−use std::collections::BTreeSet;
1210 use std::fs;
1311 use std::path::{Path, PathBuf};
14−use std::process::Command;
1512
1613 fn workspace() -> PathBuf {
1714 Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
@@ -40,108 +37,6 @@ fn sources() -> Vec<PathBuf> {
4037 files
4138 }
4239
43−#[test]
44−fn no_network_code_in_sources() {
45− // Built from pieces so this file does not trip its own check.
46− let banned = [
47− ["std", "::net"].concat(),
48− ["Tcp", "Stream"].concat(),
49− ["Tcp", "Listener"].concat(),
50− ["Udp", "Socket"].concat(),
51− ["ToSocket", "Addrs"].concat(),
52− ["AF_", "INET"].concat(),
53− ];
54− let mut hits = Vec::new();
55− for file in sources() {
56− let text = fs::read_to_string(&file).unwrap();
57− for (n, line) in text.lines().enumerate() {
58− for word in &banned {
59− if line.contains(word.as_str()) {
60− hits.push(format!("{}:{}: {word}", file.display(), n + 1));
61− }
62− }
63− }
64− }
65− assert!(
66− hits.is_empty(),
67− "network code is not allowed (CLAUDE.md rule 1):\n{}",
68− hits.join("\n")
69− );
70−}
71−
72−/// The crate names in deny.toml's `[bans] deny` list.
73−fn banned_crates() -> BTreeSet<String> {
74− let text = fs::read_to_string(workspace().join("deny.toml")).unwrap();
75− let mut names = BTreeSet::new();
76− let mut in_bans = false;
77− for line in text.lines() {
78− let line = line.trim();
79− if line.starts_with('[') {
80− in_bans = line == "[bans]";
81− continue;
82− }
83− if !in_bans {
84− continue;
85− }
86− if let Some(rest) = line.strip_prefix("{ crate = \"") {
87− let name = rest.split('"').next().unwrap();
88− names.insert(name.to_owned());
89− }
90− }
91− assert!(
92− names.contains("reqwest"),
93− "could not read the [bans] deny list from deny.toml"
94− );
95− names
96−}
97−
98−/// The crates a Linux build of the workspace uses, tests included, for the targets deny.toml
99−/// checks. Cargo.lock is no help here: it also lists crates that only other platforms build (iced's
100−/// lock entries include tokio, for wasm).
101−fn built_crates() -> BTreeSet<String> {
102− let out = Command::new(env!("CARGO"))
103− .current_dir(workspace())
104− .args(["tree", "--offline", "--locked", "--workspace", "--all-features"])
105− .args(["--edges", "normal,build,dev", "--prefix", "none", "--format", "{p}"])
106− .args([
107− "--target",
108− "x86_64-unknown-linux-gnu",
109− "--target",
110− "aarch64-unknown-linux-gnu",
111− ])
112− .output()
113− .unwrap();
114− assert!(
115− out.status.success(),
116− "cargo tree failed: {}",
117− String::from_utf8_lossy(&out.stderr)
118− );
119− let names: BTreeSet<String> = String::from_utf8(out.stdout)
120− .unwrap()
121− .lines()
122− .filter_map(|line| line.split_whitespace().next())
123− .map(str::to_owned)
124− .collect();
125− assert!(
126− names.contains("budctl-core") && names.contains("libc"),
127− "could not read crate names from cargo tree"
128− );
129− names
130−}
131−
132−#[test]
133−fn no_banned_crate_in_the_build() {
134− let built = built_crates();
135− let found: Vec<String> = banned_crates()
136− .into_iter()
137− .filter(|banned| built.contains(banned))
138− .collect();
139− assert!(
140− found.is_empty(),
141− "banned crates in the build (see deny.toml): {found:?}"
142− );
143−}
144−
14540 #[test]
14641 fn spec_references_point_at_existing_anchors() {
14742 let docs = workspace().join("docs/protocol");
D deny.toml View
@@ -1,75 +0,0 @@
1−# cargo-deny policy (`cargo deny check bans licenses sources`).
2−# CLAUDE.md hard rule 1: no network code anywhere in the dependency tree.
3−# crates/budctl-core/tests/guards.rs reads the [bans] list below and checks Cargo.lock against
4−# it, so the rule also holds where cargo-deny is not installed.
5−
6−[graph]
7−targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"]
8−all-features = true
9−
10−[advisories]
11−# Note: this check downloads the RustSec database (a dev-tool network access, not budctl's).
12−version = 2
13−
14−[licenses]
15−version = 2
16−confidence-threshold = 0.9
17−# Everything here is compatible with GPL-3.0-or-later.
18−allow = [
19− "GPL-3.0-or-later",
20− "MIT",
21− "Apache-2.0",
22− "Apache-2.0 WITH LLVM-exception",
23− "BSD-2-Clause",
24− "BSD-3-Clause",
25− "ISC",
26− "Zlib",
27− "Unicode-3.0",
28− "MPL-2.0",
29−]
30−
31−[bans]
32−multiple-versions = "warn"
33−wildcards = "deny"
34−allow-wildcard-paths = true
35−deny = [
36− # HTTP clients and servers
37− { crate = "reqwest", reason = "no network code (CLAUDE.md rule 1)" },
38− { crate = "hyper", reason = "no network code (CLAUDE.md rule 1)" },
39− { crate = "hyper-util", reason = "no network code (CLAUDE.md rule 1)" },
40− { crate = "h2", reason = "no network code (CLAUDE.md rule 1)" },
41− { crate = "h3", reason = "no network code (CLAUDE.md rule 1)" },
42− { crate = "quinn", reason = "no network code (CLAUDE.md rule 1)" },
43− { crate = "ureq", reason = "no network code (CLAUDE.md rule 1)" },
44− { crate = "surf", reason = "no network code (CLAUDE.md rule 1)" },
45− { crate = "isahc", reason = "no network code (CLAUDE.md rule 1)" },
46− { crate = "curl", reason = "no network code (CLAUDE.md rule 1)" },
47− { crate = "curl-sys", reason = "no network code (CLAUDE.md rule 1)" },
48− { crate = "attohttpc", reason = "no network code (CLAUDE.md rule 1)" },
49− { crate = "minreq", reason = "no network code (CLAUDE.md rule 1)" },
50− { crate = "tiny_http", reason = "no network code (CLAUDE.md rule 1)" },
51− { crate = "axum", reason = "no network code (CLAUDE.md rule 1)" },
52− { crate = "warp", reason = "no network code (CLAUDE.md rule 1)" },
53− { crate = "actix-web", reason = "no network code (CLAUDE.md rule 1)" },
54− { crate = "tungstenite", reason = "no network code (CLAUDE.md rule 1)" },
55− { crate = "tokio-tungstenite", reason = "no network code (CLAUDE.md rule 1)" },
56− # TLS
57− { crate = "rustls", reason = "no network code (CLAUDE.md rule 1)" },
58− { crate = "native-tls", reason = "no network code (CLAUDE.md rule 1)" },
59− { crate = "openssl", reason = "no network code (CLAUDE.md rule 1)" },
60− { crate = "openssl-sys", reason = "no network code (CLAUDE.md rule 1)" },
61− { crate = "aws-lc-rs", reason = "no network code (CLAUDE.md rule 1)" },
62− # DNS, telemetry, self-update
63− { crate = "hickory-resolver", reason = "no network code (CLAUDE.md rule 1)" },
64− { crate = "trust-dns-resolver", reason = "no network code (CLAUDE.md rule 1)" },
65− { crate = "sentry", reason = "no telemetry (CLAUDE.md non-goals)" },
66− { crate = "opentelemetry", reason = "no telemetry (CLAUDE.md non-goals)" },
67− { crate = "self_update", reason = "no update checks (CLAUDE.md non-goals)" },
68− # Keeps zbus on its own small executor instead of pulling in a whole async runtime.
69− { crate = "tokio", reason = "not needed; keep the dependency tree small" },
70−]
71−
72−[sources]
73−unknown-registry = "deny"
74−unknown-git = "deny"
75−allow-registry = ["https://github.com/rust-lang/crates.io-index"]
M docs/protocol/transport.md View
@@ -12,7 +12,7 @@ All of this is public, standard material:
1212 <a id="paired-devices"></a>
1313 ## 1. Paired devices (BlueZ over D-Bus)
1414
15−- **Connection.** budctl talks to BlueZ only over the **system bus**. The D-Bus specification gives its default address as `unix:path=/var/run/dbus/system_bus_socket`. budctl connects to that path itself and hands the stream to zbus. It never reads `DBUS_SYSTEM_BUS_ADDRESS`, which could name a TCP address (CLAUDE.md rule 1).
15+- **Connection.** budctl talks to BlueZ only over the **system bus**. The D-Bus specification gives its default address as `unix:path=/var/run/dbus/system_bus_socket`. budctl connects to that path itself and hands the stream to zbus. It never reads `DBUS_SYSTEM_BUS_ADDRESS`, which could name a TCP address.
1616 - **Listing devices.** Call `org.freedesktop.DBus.ObjectManager.GetManagedObjects` on service `org.bluez`, path `/`. Each object that has the interface `org.bluez.Device1` is a known device.
1717 - **Device1 properties used:**
1818