Rust Crate CI Guide

SkillDev tools

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.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Rust Crate CI Guide skill

What this skill tells your AI

The instructions your AI receives, as published by zaxbyhub/opencode-swarm in .claude/skills/rust-crate-ci/SKILL.md and read by ahel’s review.

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.

// rustfmt will COLLAPSE this to one line if it fits:
return Err(RunnerError::PolicyViolation {
    reason: format!(
        "cwd resolves outside allowed roots (symlink egress): {canonical_str}"
    ),
});

// rustfmt will SPLIT this if it exceeds the limit:
events::emit(&events::denial_event("deny_symlink_egress", Some(canonical_str)));
// becomes:
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~1runneradmin.

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:

CodeMeaning
0Child exited zero (or non-zero child exit code passed through directly)
64Policy violation
65Temp quota exceeded
66Wall-clock timeout
67Launcher misconfiguration or policy parse error (invalid JSON from caller)
68OS API failure, I/O error, or JSON error
69Probe 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.

Signals

GitHub stars
467
Forks
51
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
rust-crate-ci
Source
github.com/zaxbyhub/opencode-swarm