Skip to content

Commit 486d6c4

Browse files
chaliyclaude
andauthored
docs: convert doc examples to tested doctests (#504)
## Summary - Convert 14 doc examples from `rust,ignore` to compiled/tested doctests across `custom_builtins.md`, `threat-model.md`, `python.md`, and `logging.md` - Fix pre-existing doc test failures: 3 network examples using `.network()` (behind `http_client` feature) in non-feature-gated modules now correctly use `rust,ignore` - Fix incorrect `MountableFs::new()` call in `threat-model.md` to match actual API signature - Add code example fencing rules and doctest guidance to `specs/008-documentation.md` ## Test plan - [x] `cargo test --doc` (no features) — 76 passed, 0 failed - [x] `cargo test --doc --all-features` — 84 passed, 0 failed - [x] `cargo doc --no-deps` builds without errors - [x] `just check` (fmt + clippy + test) passes --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3153259 commit 486d6c4

File tree

9 files changed

+102
-32
lines changed

9 files changed

+102
-32
lines changed

crates/bashkit/docs/compatibility.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ Default limits (configurable):
380380

381381
### Network Configuration
382382

383-
```rust
383+
```rust,ignore
384384
use bashkit::{Bash, NetworkAllowlist};
385385
386386
// Enable network with URL allowlist

crates/bashkit/docs/custom_builtins.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ virtual filesystem.
1212

1313
## Quick Start
1414

15-
```rust,ignore
15+
```rust
1616
use bashkit::{Bash, Builtin, BuiltinContext, ExecResult, async_trait};
1717

1818
struct MyCommand;
@@ -144,12 +144,13 @@ pub struct ExecResult {
144144

145145
Helper constructors:
146146

147-
```rust,ignore
147+
```rust
148+
# use bashkit::ExecResult;
148149
// Success with output
149-
ExecResult::ok("output\n".to_string())
150+
ExecResult::ok("output\n".to_string());
150151

151152
// Error with message and exit code
152-
ExecResult::err("error message\n".to_string(), 1)
153+
ExecResult::err("error message\n".to_string(), 1);
153154
```
154155

155156
## Examples
@@ -221,7 +222,9 @@ impl Builtin for HttpGet {
221222

222223
Custom builtins can override default builtins by using the same name:
223224

224-
```rust,ignore
225+
```rust,no_run
226+
use bashkit::{Bash, Builtin, BuiltinContext, ExecResult, async_trait};
227+
225228
struct SecureEcho;
226229
227230
#[async_trait]
@@ -235,9 +238,11 @@ impl Builtin for SecureEcho {
235238
}
236239
}
237240
241+
# fn main() {
238242
let bash = Bash::builder()
239243
.builtin("echo", Box::new(SecureEcho)) // Overrides default echo
240244
.build();
245+
# }
241246
```
242247

243248
## Best Practices
@@ -253,7 +258,10 @@ let bash = Bash::builder()
253258
The `Builtin` trait requires `Send + Sync`. For builtins with mutable state, use
254259
appropriate synchronization:
255260

256-
```rust,ignore
261+
```rust
262+
use bashkit::{Builtin, BuiltinContext, ExecResult, async_trait};
263+
use std::sync::Arc;
264+
257265
struct Counter {
258266
count: Arc<std::sync::atomic::AtomicU64>,
259267
}

crates/bashkit/docs/logging.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,10 @@ RUST_LOG=bashkit::parser=debug cargo run
7979

8080
### Custom Configuration
8181

82-
```rust,ignore
82+
```rust
8383
use bashkit::{Bash, LogConfig};
8484

85+
# fn main() {
8586
let bash = Bash::builder()
8687
.log_config(LogConfig::new()
8788
// Add custom sensitive variable patterns
@@ -90,6 +91,7 @@ let bash = Bash::builder()
9091
// Limit logged value lengths
9192
.max_value_length(100))
9293
.build();
94+
# }
9395
```
9496

9597
## Security (TM-LOG-*)
@@ -130,7 +132,8 @@ Control characters are filtered, and newlines are escaped.
130132

131133
For debugging in **non-production** environments only:
132134

133-
```rust,ignore
135+
```rust
136+
# use bashkit::LogConfig;
134137
// WARNING: May expose sensitive data
135138
let config = LogConfig::new()
136139
.unsafe_disable_redaction() // Disable ALL redaction

crates/bashkit/docs/python.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@ configurable resource limits and no host access.
1818

1919
Enable the `python` feature and register via builder:
2020

21-
```rust,ignore
21+
```rust
2222
use bashkit::Bash;
2323

24+
# #[tokio::main]
25+
# async fn main() -> bashkit::Result<()> {
2426
let mut bash = Bash::builder().python().build();
2527

2628
let result = bash.exec("python3 -c \"print('hello from Monty')\"").await?;
2729
assert_eq!(result.stdout, "hello from Monty\n");
30+
# Ok(())
31+
# }
2832
```
2933

3034
## Usage Patterns
@@ -120,10 +124,11 @@ resumes execution with the result (or a Python exception like `FileNotFoundError
120124

121125
Default limits prevent runaway Python code. Customize via `PythonLimits`:
122126

123-
```rust,ignore
127+
```rust,no_run
124128
use bashkit::{Bash, PythonLimits};
125129
use std::time::Duration;
126130
131+
# fn main() {
127132
let bash = Bash::builder()
128133
.python_with_limits(
129134
PythonLimits::default()
@@ -133,6 +138,7 @@ let bash = Bash::builder()
133138
.max_recursion(50)
134139
)
135140
.build();
141+
# }
136142
```
137143

138144
| Limit | Default | Purpose |
@@ -146,15 +152,17 @@ let bash = Bash::builder()
146152

147153
When using `BashTool` for AI agents, call `.python()` on the tool builder:
148154

149-
```rust,ignore
150-
use bashkit::BashTool;
155+
```rust,no_run
156+
use bashkit::{BashTool, Tool};
151157
158+
# fn main() {
152159
let tool = BashTool::builder()
153160
.python()
154161
.build();
155162
156163
// help() and system_prompt() automatically document Python limitations
157164
let help = tool.help(); // Includes NOTES section with Python hints
165+
# }
158166
```
159167

160168
The builtin's `llm_hint()` is automatically included in the tool's documentation,

crates/bashkit/docs/threat-model.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,12 @@ through configurable limits.
4545
| ExtGlob blowup (TM-DOS-031) | `+(a\|aa)` exponential | Add depth limit | **OPEN** |
4646

4747
**Configuration:**
48-
```rust,ignore
48+
```rust
4949
use bashkit::{Bash, ExecutionLimits, FsLimits, InMemoryFs};
5050
use std::sync::Arc;
5151
use std::time::Duration;
5252

53+
# fn main() {
5354
let limits = ExecutionLimits::new()
5455
.max_commands(10_000)
5556
.max_loop_iterations(10_000)
@@ -67,6 +68,7 @@ let bash = Bash::builder()
6768
.limits(limits)
6869
.fs(fs)
6970
.build();
71+
# }
7072
```
7173

7274
### Sandbox Escape (TM-ESC-*)
@@ -88,17 +90,19 @@ Scripts may attempt to break out of the sandbox to access the host system.
8890
Bashkit uses an in-memory virtual filesystem by default. Scripts cannot access the
8991
real filesystem unless explicitly mounted via [`MountableFs`].
9092

91-
```rust,ignore
92-
use bashkit::{Bash, InMemoryFs};
93+
```rust
94+
use bashkit::{Bash, InMemoryFs, MountableFs};
9395
use std::sync::Arc;
9496

97+
# fn main() {
9598
// Default: fully isolated in-memory filesystem
9699
let bash = Bash::new();
97100

98101
// Custom filesystem with explicit mounts (advanced)
99-
use bashkit::MountableFs;
100-
let fs = Arc::new(MountableFs::new());
101-
// fs.mount_readonly("/data", "/real/path/to/data"); // Optional real FS access
102+
let root = Arc::new(InMemoryFs::new());
103+
let fs = Arc::new(MountableFs::new(root));
104+
// fs.mount("/data", Arc::new(InMemoryFs::new())); // Mount additional filesystems
105+
# }
102106
```
103107

104108
### Information Disclosure (TM-INF-*)
@@ -118,7 +122,8 @@ Scripts may attempt to leak sensitive information.
118122

119123
Do NOT pass sensitive environment variables to untrusted scripts:
120124

121-
```rust,ignore
125+
```rust
126+
# use bashkit::Bash;
122127
// UNSAFE - secrets may be leaked
123128
let bash = Bash::builder()
124129
.env("DATABASE_URL", "postgres://user:pass@host/db")
@@ -136,7 +141,8 @@ let bash = Bash::builder()
136141

137142
System builtins return configurable virtual values, never real host information:
138143

139-
```rust,ignore
144+
```rust
145+
# use bashkit::Bash;
140146
let bash = Bash::builder()
141147
.username("sandbox") // whoami returns "sandbox"
142148
.hostname("bashkit-sandbox") // hostname returns "bashkit-sandbox"
@@ -227,10 +233,11 @@ echo $user_input
227233
Each [`Bash`] instance is fully isolated. For multi-tenant environments, create
228234
separate instances per tenant:
229235

230-
```rust,ignore
236+
```rust
231237
use bashkit::{Bash, InMemoryFs};
232238
use std::sync::Arc;
233239

240+
# fn main() {
234241
// Each tenant gets completely isolated instance
235242
let tenant_a = Bash::builder()
236243
.fs(Arc::new(InMemoryFs::new())) // Separate filesystem
@@ -241,6 +248,7 @@ let tenant_b = Bash::builder()
241248
.build();
242249

243250
// tenant_a cannot access tenant_b's files or state
251+
# }
244252
```
245253

246254
### Internal Error Handling (TM-INT-*)

crates/bashkit/src/lib.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -243,11 +243,9 @@
243243
//!
244244
//! Enable the `http_client` feature and configure an allowlist for network access:
245245
//!
246-
//! ```rust,no_run
246+
//! ```rust,ignore
247247
//! use bashkit::{Bash, NetworkAllowlist};
248248
//!
249-
//! # #[tokio::main]
250-
//! # async fn main() -> bashkit::Result<()> {
251249
//! let mut bash = Bash::builder()
252250
//! .network(NetworkAllowlist::new()
253251
//! .allow("https://httpbin.org"))
@@ -256,8 +254,6 @@
256254
//! // curl and wget now work for allowed URLs
257255
//! let result = bash.exec("curl -s https://httpbin.org/get").await?;
258256
//! assert!(result.stdout.contains("httpbin.org"));
259-
//! # Ok(())
260-
//! # }
261257
//! ```
262258
//!
263259
//! Security features:

crates/bashkit/src/network/mod.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@
1818
//!
1919
//! Configure network access using [`NetworkAllowlist`] with [`crate::Bash::builder`]:
2020
//!
21-
//! ```rust,no_run
21+
//! ```rust,ignore
2222
//! use bashkit::{Bash, NetworkAllowlist};
2323
//!
24-
//! # #[tokio::main]
25-
//! # async fn main() -> bashkit::Result<()> {
2624
//! let mut bash = Bash::builder()
2725
//! .network(NetworkAllowlist::new()
2826
//! .allow("https://api.example.com")
@@ -31,8 +29,6 @@
3129
//!
3230
//! // Now curl/wget can access allowed URLs
3331
//! let result = bash.exec("curl -s https://api.example.com/data").await?;
34-
//! # Ok(())
35-
//! # }
3632
//! ```
3733
//!
3834
//! # Allowlist Patterns

specs/008-documentation.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,47 @@ Add "Guides" section and link to scorecard in main crate documentation:
8484
4. Add link in crate docs `# Guides` section
8585
5. Run `cargo doc --open` to verify
8686

87+
## Code Examples
88+
89+
Rust code examples in guides are compiled and tested by `cargo test --doc`.
90+
91+
### Fencing rules
92+
93+
| Fence | When to use |
94+
|-------|-------------|
95+
| `` ```rust `` | Complete examples using only bashkit types — tested |
96+
| `` ```rust,no_run `` | Complete examples that compile but shouldn't execute |
97+
| `` ```rust,ignore `` | Uses external crates (sqlx, reqwest, tracing-subscriber) or feature-gated APIs in non-gated modules |
98+
99+
### Making examples testable
100+
101+
Use `# ` (hash-space) prefix to hide boilerplate lines from rendered docs while
102+
keeping them in the compiled test:
103+
104+
````markdown
105+
```rust
106+
# use bashkit::Bash;
107+
# #[tokio::main]
108+
# async fn main() -> bashkit::Result<()> {
109+
let mut bash = Bash::new();
110+
let result = bash.exec("echo hello").await?;
111+
assert_eq!(result.stdout, "hello\n");
112+
# Ok(())
113+
# }
114+
```
115+
````
116+
117+
### Feature-gated modules
118+
119+
Doc modules behind `#[cfg(feature = "...")]` (e.g., `python_guide`, `logging_guide`)
120+
can use feature-gated APIs freely — their tests only run when the feature is enabled.
121+
122+
Non-gated modules (e.g., `threat_model`, `compatibility_scorecard`) must NOT use
123+
feature-gated APIs in tested examples. Use `rust,ignore` for those.
124+
87125
## Verification
88126

89127
- `cargo doc` builds without errors
128+
- `cargo test --doc --all-features` passes
90129
- Links resolve correctly in generated docs
91130
- Markdown renders properly in rustdoc

supply-chain/config.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,10 @@ criteria = "safe-to-deploy"
442442
version = "0.4.1"
443443
criteria = "safe-to-run"
444444

445+
[[exemptions.getrandom]]
446+
version = "0.4.2"
447+
criteria = "safe-to-run"
448+
445449
[[exemptions.globset]]
446450
version = "0.4.18"
447451
criteria = "safe-to-deploy"
@@ -910,6 +914,10 @@ criteria = "safe-to-deploy"
910914
version = "5.3.0"
911915
criteria = "safe-to-deploy"
912916

917+
[[exemptions.r-efi]]
918+
version = "6.0.0"
919+
criteria = "safe-to-deploy"
920+
913921
[[exemptions.rand]]
914922
version = "0.8.5"
915923
criteria = "safe-to-deploy"
@@ -1270,6 +1278,10 @@ criteria = "safe-to-deploy"
12701278
version = "1.49.0"
12711279
criteria = "safe-to-deploy"
12721280

1281+
[[exemptions.tokio]]
1282+
version = "1.50.0"
1283+
criteria = "safe-to-deploy"
1284+
12731285
[[exemptions.tokio-macros]]
12741286
version = "2.6.0"
12751287
criteria = "safe-to-deploy"

0 commit comments

Comments
 (0)