| name | rust-crate-ci |
| audience | swarm-plugin |
| description | Load before editing any Rust crate in this repo (currently runners/swarm-sandbox-runner). Covers the mandatory local validation gate, common rustfmt/clippy pitfalls, and Windows-specific Rust correctness patterns that CI enforces but are hard to catch locally without a Windows toolchain.
|
| effort | low |
Rust Crate CI Guide
This repo contains a Rust crate at runners/swarm-sandbox-runner/. The CI job that
validates it is rust-sandbox-runner (runs on windows-latest). This guide prevents
the most common failure modes before they reach CI.
Mandatory local gate (run in order before pushing)
cd runners/swarm-sandbox-runner
# 1. Format check — CI fails here first; clippy will not run if this fails
cargo fmt --check
# 2. Clippy — -D warnings makes all warnings hard errors; --all-targets matches CI,
# which also lints tests, examples, and benches (ci.yml rust-sandbox-runner job)
cargo clippy --all-targets -- -D warnings
# 3. Tests — --all-targets matches CI; Windows-specific tests are gated with #[cfg(windows)]
cargo test --all-targets
# 4. Release build — confirms the binary compiles with optimizations
cargo build --release
Run them in this exact order. If cargo fmt --check fails, fix formatting first —
clippy errors may be masked until fmt passes.
If cargo is not in PATH locally (e.g. you are on a machine without Rust installed),
push to a draft PR and let CI run the checks. Read the CI log carefully; do not guess
at what failed.
How rustfmt makes decisions
rustfmt (current stable — rust-toolchain.toml floats on channel = "stable")
applies line-length thresholds per syntax item, not per file or per block.
Two patterns that look equivalent locally can format differently:
Long format! macros: If the total length of format!("...", arg) exceeds the
line limit, rustfmt splits it. If it fits on one line, rustfmt collapses it.
return Err(RunnerError::PolicyViolation {
reason: format!(
"cwd resolves outside allowed roots (symlink egress): {canonical_str}"
),
});
events::emit(&events::denial_event("deny_symlink_egress", Some(canonical_str)));
events::emit(&events::denial_event(
"deny_symlink_egress",
Some(canonical_str),
));
Rule of thumb: After editing, always run cargo fmt (not --check) locally to
apply rustfmt's opinion, then read the diff before committing. Do not hand-format
and assume rustfmt agrees.
Common clippy errors in this codebase
windows-rs 0.58 API changes: Several Win32 APIs changed signatures between
windows crate versions. The Cargo.toml pins the version; do not bump it without
checking the full API diff.
HANDLE is not Send + Sync: Raw HANDLE values cannot be sent across threads.
Wrap them in a newtype with unsafe impl Send and unsafe impl Sync, or use
OwnedHandle from std::os::windows::io. The kill-callback pattern in
temp_watcher.rs uses raw isize (handle as integer) to sidestep this.
Unused imports: #[cfg(windows)] blocks frequently hide imports that are used
only on Windows. On non-Windows builds, the import becomes dead code. Use
#[cfg(windows)] on the use statement itself or accept the #[allow(unused_imports)]
annotation for platform-conditional code.
struct default initialization: For Win32 structs like STARTUPINFOW, use a
struct initializer with ..Default::default() rather than unsafe { std::mem::zeroed() }
— clippy flags the latter when Default is available.
Windows path normalization (critical correctness patterns)
These bugs are invisible on Linux/macOS CI and will only appear on windows-latest.
std::fs::canonicalize always prepends \\?\
On Windows, canonicalize typically returns a verbatim extended-length path with
the \\?\ prefix, but this is not guaranteed across all Windows configurations and
path forms. Always use .strip_prefix with a fallback:
C:\foo\bar → \\?\C:\foo\bar (typical)
If you compare a canonicalized path against a policy root stored without this prefix,
the comparison silently fails. Strip the prefix before comparing, always with .unwrap_or:
let lower = canonical.to_string_lossy().to_lowercase();
let canonical_str = lower.strip_prefix("\\\\?\\").unwrap_or(&lower);
Apply the same strip to both sides of any comparison.
GitHub Actions CI uses 8.3 short names in %TEMP%
std::env::temp_dir() on GitHub Actions Windows runners returns a path like:
C:\Users\RUNNER~1\AppData\Local\Temp
But std::fs::canonicalize expands it to:
C:\Users\runneradmin\AppData\Local\Temp
If your code stores the temp_dir() path as a policy root and then canonicalizes
the cwd, the comparison will fail because RUNNER~1 ≠ runneradmin.
Fix: Canonicalize both the policy root AND the cwd before comparing. Fall back
to the raw value if the root does not exist on disk yet:
let root_resolved = std::fs::canonicalize(root)
.map(|p| p.to_string_lossy().to_lowercase())
.unwrap_or_else(|_| root.to_lowercase());
let root_cmp = root_resolved
.strip_prefix("\\\\?\\")
.unwrap_or(&root_resolved)
.to_owned();
WaitForSingleObject return value semantics
WAIT_TIMEOUT is 0x00000102u32, not a Rust Err. Check the return value
explicitly before treating the child as having exited normally.
IPC contract stability
The exit codes in error.rs are frozen — do not renumber them. TypeScript
callers in runner-client.ts depend on these values:
| Code | Meaning |
|---|
| 0 | Child exited zero (or non-zero child exit code passed through directly) |
| 64 | Policy violation |
| 65 | Temp quota exceeded |
| 66 | Wall-clock timeout |
| 67 | Launcher misconfiguration or policy parse error (invalid JSON from caller) |
| 68 | OS API failure, I/O error, or JSON error |
| 69 | Probe failed |
The NDJSON event types on stderr (start, denial, quota_exceeded, exit) are
also frozen. Adding new event types is safe; renaming or removing existing ones
is a breaking IPC change.
Exit events are required on every termination path. Emit exit_event before
returning from both the wall-clock timeout path and the temp-cap kill path, not
only on the normal exit path. TypeScript consumers block waiting for an exit event.
CI workflow reference
The job is defined in .github/workflows/ci.yml as rust-sandbox-runner.
It runs on windows-latest with the toolchain pinned in runners/swarm-sandbox-runner/rust-toolchain.toml.
All third-party uses: actions are pinned to 40-character SHAs per the repo policy.
Steps in order: fmt check → clippy → tests → release build → probe smoke test.