# ADR-0015: Phase 7d.3 — rust / crates.io supply chain audit

**Status:** Accepted
**Date:** 2026-05-11
**Related:** ADR-0014 (Phase 7d.2 go + common extraction), ADR-0013 (Phase 7d.1 pip), ADR-0012 (Phase 7d npm)

## Context

Phase 7d.2 (v0.14.0) shipped go supply chain audit AND extracted
`_supply_chain_common.py` into fetcher-side + check-side modules.
Phase 7d.3 is the "investment payoff" — adding the 4th ecosystem
(Rust / crates.io) with zero new abstraction work, just direct import
of the existing common helpers.

Spec: `docs/superpowers/specs/2026-05-11-phase-7d3-rust-supply-chain-design.md`.

## Decisions

### D1. Lockfile paths come from operator config (mirroring 7d.1/7d.2)

**Decision:** Operator lists each `Cargo.lock` to scan in `ServerAuditConfig.cargo_lock_paths` (or repeats `--cargo-lock-path` on the standalone `agentsec server-audit` command). Empty list → `rust_supply_chain_audit` reports `not_applicable`.

**Considered alternatives:**
- (a) Add a `cargo_root` key to `PlatformProfile` — OpenClaw is npm-based, no canonical Rust footprint.
- (b) Scan the host's installed Cargo binaries via `cargo metadata` — requires executing `cargo` CLI.

**Rationale:** Operator config = consent. Consistent with 7d.1 / 7d.2.

### D2. Format = `Cargo.lock` only; basename strict equality, case-sensitive

**Decision:** Validator requires `basename(path) == "Cargo.lock"` exactly. `cargo.lock` (lowercase) and `Cargo.lock.bak` rejected.

**Considered alternatives:**
- (a) Case-insensitive match — would accept user-typo `cargo.lock`.
- (b) Allow `Cargo.lock.*` variants.

**Rationale:** Cargo official convention is exactly `Cargo.lock` with leading capital. Strict equality is simplest defensible rule.

### D3. Crate names NOT normalized

**Decision:** Crate names from `Cargo.lock` and from OSV-Cargo records are kept as-is for matching. Unlike pip (PEP-503 normalizes `Foo_Bar` → `foo-bar`), Cargo treats `serde-json` and `serde_json` as completely different crates.

**Considered alternatives:**
- (a) Normalize to lowercase + collapse `[-_]` like PEP-503 — would create false matches between `serde-json` and `serde_json`.

**Rationale:** Cargo's own naming semantics.

### D4. Cargo versions use strict semver 2.0; no prefix/suffix cleanup

**Decision:** `_safe_parse_cargo_version(s)` just calls `semver.Version.parse(s)`. No `v` prefix stripping (unlike Go), no `+incompatible` handling (Cargo doesn't use this).

**Considered alternatives:**
- (a) Mirror Go's strip-and-parse pattern defensively.

**Rationale:** Cargo enforces strict semver. Any pre-processing would be dead code.

### D5. Filter to `source = registry+...crates.io-index` only

**Decision:** `parse_cargo_lock` only includes packages whose `source` field equals the official crates.io registry URL. Workspace-local (no `source`) / git deps / path deps / alternate registries silently skipped.

**Considered alternatives:**
- (a) Also emit Low findings for non-crates.io deps — would generate noise for legitimate uses (workspace crates, private registries).
- (b) Include alternate registries — would require operator to configure registry URLs.

**Rationale:** OSV-Cargo only covers crates.io. Other sources aren't in the public advisory database; emitting findings for them would be misleading. Consistent with pip silently skipping VCS URL pins.

### D6. Direct import `_supply_chain_common`, zero new abstraction

**Decision:** `RustSupplyChainAuditCheck` imports `load_advisory_db` / `Severity` from `checks/_supply_chain_common.py`. `OsvCargoFetcher` imports `_is_within_age` / `_extract_severity` / `MEDIUM_LOW_MAX_AGE_DAYS` from `fetchers/_supply_chain_common.py`. No new shared module work.

**Considered alternatives:**
- (a) Re-evaluate common module at N=4 — confirmed all helpers still applicable.
- (b) Generalize the matcher across all 4 ecosystems now that we have 4 — still rejected per ADR-0014 D5 (Version types still differ).

**Rationale:** "Rule of three" investment from 7d.2 paying off. N=4 doesn't change the analysis.

### D7. exact-kind dynamic injection (mirrors 7d.1 / 7d.2)

**Decision:** `policy.add_runtime_allowed_paths(operator_paths, kind="exact")` + audit-log entry.

**Rationale:** Same as prior phases. Minimal blast radius.

### D8. OSV-Cargo 5-axis filter chain identical to npm/pypi/go

**Decision:** `normalize_osv_cargo_entry` applies the same 5 filter axes: drop withdrawn / no-crates.io-affected / no-range / no-CVSS / scored medium-low > 2 years old.

**Rationale:** Same heuristics validated across npm/pypi/go. Single shared filter logic via `_supply_chain_common.py`.

### D9. TI = single umbrella `TI-OSV-CRATES-CATALOG`

**Decision:** One `threat_intel.yaml` row covers every Cargo advisory.

**Rationale:** Same pattern as `TI-OSV-NPM-CATALOG` / `TI-OSV-PYPI-CATALOG` / `TI-OSV-GO-CATALOG`.

## Consequences

- Four ecosystems share `_supply_chain_common.py`; no abstraction debt accumulating.
- The wheel grows by ~545 KB (osv_cargo.json bundled). Total wheel size still well below 15 MB cap.
- Pattern is now mature; adding a 5th ecosystem (if ever) is purely additive.
- 7d.2 final-review follow-ups (8 items) explicitly NOT addressed in this phase; tracked for separate cleanup commit.

## References

- Spec: `docs/superpowers/specs/2026-05-11-phase-7d3-rust-supply-chain-design.md`
- Plan: `docs/superpowers/plans/2026-05-11-phase-7d3-rust-supply-chain.md`
- ADR-0014: 7d.2 (go) — extracted the common modules this phase consumes
- ADR-0013: 7d.1 (pip) — established operator-config-path pattern
- ADR-0012: 7d (npm) — original supply-chain Check design
