diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 7893df28..d60c5241 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -4,9 +4,9 @@ # real vulnerabilities fail the job. [advisories] ignore = [ - # Marvin attack advisory covers every released version of rsa (introduced 0.0.0-0, no patch). - # sharecli only decodes/verifies JWTs with RSA public keys for local sidecar auth (src/serve_auth.rs). - "RUSTSEC-2023-0071", - # Informational 'unmaintained' advisory for paste - stale Cargo.lock entry. - "RUSTSEC-2024-0436", + # Marvin attack advisory covers every released version of rsa (introduced 0.0.0-0, no patch). + # sharecli only decodes/verifies JWTs with RSA public keys for local sidecar auth (src/serve_auth.rs). + "RUSTSEC-2023-0071", + # Informational 'unmaintained' advisory for paste - stale Cargo.lock entry. + "RUSTSEC-2024-0436", ] diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index 41da304e..636c09bf 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -3,15 +3,8 @@ # Threshold contract: docs/ops/mutants-threshold.md # Schema: cargo-mutants ≥25 (examine_globs + exclude_re; no jobs/timeout keys) -examine_globs = [ - "crates/sharecli-thermal-tui/src/lib.rs", -] +examine_globs = ["crates/sharecli-thermal-tui/src/lib.rs"] -exclude_re = [ - "render", - "event_loop", - "replace run ", - "count_cargo_builds", -] +exclude_re = ["render", "event_loop", "replace run ", "count_cargo_builds"] test_package = ["sharecli-thermal-tui"] diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 00000000..13aa0765 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,14 @@ +# actionlint configuration +# https://github.com/rhysd/actionlint/blob/v1.7.12/docs/config.md +# Note: the repository self-hosts no runners; all workflows run on GitHub-hosted +# images, so no self-hosted-runner labels are declared here. + +paths: + # Forward reference: the `detect` job's `outputs:` block references + # `steps.detect.outputs.*` before the step with `id: detect` is declared. + # This is legal in GitHub Actions (step ids resolve within the whole job) and + # the detect job is exercised on every CI run, but actionlint's static + # analysis reports it as an undefined property. Ignore that specific message. + .github/workflows/ci.yml: + ignore: + - 'property "detect" is not defined in object type' diff --git a/.github/workflows/a11y.yml b/.github/workflows/a11y.yml index 95453d52..e92654f2 100644 --- a/.github/workflows/a11y.yml +++ b/.github/workflows/a11y.yml @@ -27,7 +27,7 @@ jobs: - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: '20' + node-version: '22' cache: npm - name: Install a11y deps @@ -57,7 +57,7 @@ jobs: - name: Build sharecli run: cargo build --locked --release -p sharecli - name: Install Playwright Chromium - run: npx --yes playwright@1.49.0 install --with-deps chromium + run: npx --yes playwright@1.62.1 install --with-deps chromium - name: Run keyboard Tab-cycle env: SHARECLI_VISUAL_FIXTURE: "1" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec1ebad2..bec33e13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -306,7 +306,7 @@ jobs: "go:${{ needs.go.result }}" \ "typescript:${{ needs.typescript.result }}" \ "security:${{ needs.security.result }}" \ - "dep-review:${{ needs.dependency-review.result }}" \ + "dep-review:${{ needs.dep-review.result }}" \ ; do name="${pair%%:*}" result="${pair#*:}" diff --git a/.github/workflows/fuse-mount-smoke.yml b/.github/workflows/fuse-mount-smoke.yml index a1f5a3b6..8ab46b38 100644 --- a/.github/workflows/fuse-mount-smoke.yml +++ b/.github/workflows/fuse-mount-smoke.yml @@ -32,8 +32,15 @@ jobs: steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable + - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 + with: + version: 0.14.1 - name: Install fuse3 - run: sudo apt-get update && sudo apt-get install -y fuse3 libfuse3-dev pkg-config + run: | + sudo apt-get update && sudo apt-get install -y fuse3 libfuse3-dev pkg-config + # The mount smoke uses allow_other (multi-surface mounts); fuse3 + # refuses it unless user_allow_other is set in /etc/fuse.conf. + sudo sh -c 'grep -q "^user_allow_other" /etc/fuse.conf || echo "user_allow_other" >> /etc/fuse.conf' - name: Privileged mount smoke env: SHARECLI_FUSE_MOUNT_SMOKE: '1' diff --git a/.github/workflows/infisical.yml b/.github/workflows/infisical.yml index a46d4960..47a247be 100644 --- a/.github/workflows/infisical.yml +++ b/.github/workflows/infisical.yml @@ -16,7 +16,7 @@ on: jobs: sync-secrets: name: Pull secrets from Infisical - runs-on: blacksmith-2vcpu-ubuntu-2204 + runs-on: ubuntu-22.04 timeout-minutes: 5 steps: - name: Checkout @@ -56,4 +56,4 @@ jobs: with: name: infisical-debug path: .env - retention-days: 1 \ No newline at end of file + retention-days: 1 diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml index f065de67..a06e0cb4 100644 --- a/.github/workflows/sast.yml +++ b/.github/workflows/sast.yml @@ -4,6 +4,9 @@ on: branches: [main, develop] pull_request: schedule: [{cron: "0 2 * * *"}] +permissions: + contents: read + security-events: write jobs: codeql: runs-on: ubuntu-24.04 @@ -11,6 +14,8 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: github/codeql-action/init@v4 with: - languages: typescript, python, rust, go + # Go was removed: the repo contains no Go sources, and CodeQL aborts + # database finalization when a configured language has no files. + languages: typescript, python, rust - uses: github/codeql-action/autobuild@v4 - uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a9de6257..db717ea1 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -15,7 +15,12 @@ on: push: branches: [main] -permissions: read-all +permissions: + contents: read + # Required by scorecard-action's `publish_results` (Fulcio signing via OIDC); + # `read-all` (previous value) blocked the token and failed the run with + # "error obtaining token: expired_token" during result signing. + id-token: write jobs: analysis: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 56262e63..56fb6012 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -56,7 +56,7 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - + - name: Setup Rust uses: dtolnay/rust-toolchain@stable - name: Setup Zig @@ -72,8 +72,6 @@ jobs: with: file: Cargo.lock - - # Dependency Vulnerability Scanning dependencies: name: Dependency Audit @@ -82,7 +80,7 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - + - name: Setup Rust uses: dtolnay/rust-toolchain@stable @@ -91,8 +89,6 @@ jobs: cargo install cargo-audit cargo audit - - # Container Scanning (if Dockerfile exists) container: name: Container Scan diff --git a/.trunk/configs/.yamllint.yaml b/.trunk/configs/.yamllint.yaml new file mode 100644 index 00000000..3f9e6fb2 --- /dev/null +++ b/.trunk/configs/.yamllint.yaml @@ -0,0 +1,35 @@ +# Yamllint configuration used by Trunk Check (docs: https://yamllint.readthedocs.io/) +# Relaxations vs yamllint defaults: +# - line-length: the repo's workflows/docs carry long URLs and expressions +# (GitHub `${{ }}` interpolations, SHA-pinned action refs). Enforced for +# `.md`-free YAML would generate noise; disabled repo-wide. +# - comments: hash comments (e.g. `# v7.0.1` next to pinned actions) may be +# indented freely; only require a space after the hash. +# - document-start: workflows start with `name:` and no `---`; not required. +# - truthy: `on:` is parsed as a boolean key by some YAML 1.1 parsers. +# Allowed explicitly to keep `on:`-based workflow triggers clean. + +extends: default + +rules: + line-length: disable + comments: + require-starting-space: true + min-spaces-from-content: 1 + comments-indentation: disable + document-start: disable + truthy: + allowed-values: + - "true" + - "false" + - "on" + - "off" + check-keys: false + brackets: + max-spaces-inside: 1 + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + indentation: + spaces: consistent + indent-sequences: whatever diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index fbb1fbcb..c749eb0e 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -1,113 +1,49 @@ # ============================================================================= -# Trunk.io — Plugin versions for linting/formatting tools +# Trunk.io — Unified linting/formatting in GitHub Actions # ============================================================================= -# This file is auto-generated. Run `trunk upgrade` to update. -# https://docs.trunk.io/check/reference +# Schema: https://docs.trunk.io/check/reference +# +# Scope note: this lane is deliberately lean. The repo's heavier quality gates +# already run as dedicated workflows (cargo fmt + clippy in security.yml / +# ci-gate.yml, cargo test in the release matrix), and the nested lib/teamcomm +# workspace is not formatted by the root `cargo fmt` (40+ files). This lane +# lints the things no other lane covers: workflow YAML (actionlint), TOML +# formatting (taplo), and YAML style (yamllint, relaxed via +# .trunk/configs/.yamllint.yaml). # ============================================================================= +version: 0.1 plugins: sources: - id: trunk - ref: v1.2.2 - - id: community - ref: main + uri: https://github.com/trunk-io/plugins + ref: v1.6.7 -# Linters (auto-detected by file type) -linters: - actionlint: - enabled: true - commands: - - name: actionlint - run: actionlint ${target} - direct_configs: - - .github/workflows/*.yml - black: - enabled: true - commands: - - name: black - run: black --check --line-length 100 ${target} - direct_configs: - - pyproject.toml - - ruff.toml - clippy: - enabled: true - commands: - - name: clippy - run: cargo clippy --all-targets --all-features -- -D warnings - eslint: - enabled: true - direct_configs: - - .eslintrc.* - - eslint.config.* - golangci-lint: - enabled: true - direct_configs: - - .golangci.yml - - .golangci.yaml - mypy: - enabled: true - commands: - - name: mypy - run: mypy --ignore-missing-imports ${target} - ruff: - enabled: true - commands: - - name: ruff - run: ruff check --output-format=github ${target} - direct_configs: - - ruff.toml - - pyproject.toml - shellcheck: - enabled: true - taplo: - enabled: true - yamllint: - enabled: true +cli: + version: 1.22.2 -# Formatters -formatters: - black: - enabled: true - commands: - - name: black - run: black --line-length 100 ${target} - direct_configs: - - pyproject.toml - prettier: - enabled: true - direct_configs: - - .prettierrc - - prettier.config.* - rustfmt: - enabled: true - commands: - - name: rustfmt - run: rustfmt ${target} +lint: + enabled: + - actionlint + - taplo + - yamllint + disabled: [] + # The `detect` job's `outputs:` block references `steps.detect.outputs.*` + # before the step with `id: detect` is declared. This is legal in GitHub + # Actions (step ids resolve within the whole job; the detect job runs on + # every CI run), but actionlint's static analysis reports it as an undefined + # property. Ignore that specific message in ci.yml only. + ignore: + - linters: [actionlint] + paths: + - .github/workflows/ci.yml -# Actions (CI optimization) actions: - trunk-check: - enabled: true - size: 5GB - memory: 16GB - disk: 10GB - trunk-merge: - enabled: true - size: 5GB - trunk-push: - enabled: true - size: 5GB - -# Caching -cache: - enabled: true - storage: local - -# CLI -cli: - version: 1.22.2 + enabled: [] + disabled: + - trunk-announce + - trunk-cache-prune + - trunk-upgrade-available -# Environment -env: - variables: - EDITOR: vim +tools: + enabled: [] diff --git a/Cargo.toml b/Cargo.toml index 43a8826a..a2cf9de8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,11 @@ libc = "0.2" anyhow = "1" thiserror = "2" tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] } +tracing-subscriber = { version = "0.3", features = [ + "env-filter", + "fmt", + "json", +] } tracing-appender = "0.2" # Config @@ -78,7 +82,11 @@ sha2 = "0.11" opentelemetry = { version = "0.32", features = ["trace"] } # 0.32.1+ patches RustSec RUSTSEC-2026-* unbounded W3C Baggage allocation. opentelemetry_sdk = { version = "0.32.1", features = ["rt-tokio", "trace"] } -opentelemetry-otlp = { version = "0.32", features = ["http-proto", "reqwest-rustls", "trace"] } +opentelemetry-otlp = { version = "0.32", features = [ + "http-proto", + "reqwest-rustls", + "trace", +] } tracing-opentelemetry = "0.33" jsonwebtoken = { version = "11", features = ["use_pem", "rust_crypto"] } @@ -192,20 +200,20 @@ strip = true [workspace] members = [ - "crates/harness-native", - "crates/sharecli-sync", - "crates/sharecli-ipc", - "crates/sharecli-ffi", - "crates/sharecli-tray-linux", - "crates/sharecli-tray-windows", - "crates/sharecli-fleet", - "crates/sharecli-mesh", - "crates/spawn-core-sys", - "crates/sharecli-fuse", - "crates/sharecli-core", - "crates/sharecli-thermal-tui", - "crates/fuse-smoke-runner", - "crates/sharecli-session", + "crates/harness-native", + "crates/sharecli-sync", + "crates/sharecli-ipc", + "crates/sharecli-ffi", + "crates/sharecli-tray-linux", + "crates/sharecli-tray-windows", + "crates/sharecli-fleet", + "crates/sharecli-mesh", + "crates/spawn-core-sys", + "crates/sharecli-fuse", + "crates/sharecli-core", + "crates/sharecli-thermal-tui", + "crates/fuse-smoke-runner", + "crates/sharecli-session", ] # --------------------------------------------------------------------------- @@ -218,11 +226,11 @@ members = [ [workspace.metadata.dist] dist = true targets = [ - "aarch64-apple-darwin", - "x86_64-apple-darwin", - "x86_64-unknown-linux-gnu", - "x86_64-pc-windows-msvc", - "aarch64-pc-windows-msvc", + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", + "aarch64-pc-windows-msvc", ] # Apps built by cargo-dist (all Rust workspace binaries we ship). # The Swift tray + WinUI 3 tray are added by the workflow. diff --git a/Containerfile.fuse-smoke b/Containerfile.fuse-smoke index d7fce5a1..8331c12f 100644 --- a/Containerfile.fuse-smoke +++ b/Containerfile.fuse-smoke @@ -15,6 +15,9 @@ RUN apt-get update \ WORKDIR /src # Minimal cargo workspace for fuse crate only COPY crates/sharecli-fuse/Cargo.toml crates/sharecli-fuse/Cargo.toml +# Cargo requires the declared build script to be present even though build.rs +# only emits macOS-specific link flags (no-op on the Debian builder). +COPY crates/sharecli-fuse/build.rs crates/sharecli-fuse/build.rs COPY crates/sharecli-fuse/src crates/sharecli-fuse/src # Synthetic workspace so cargo can resolve the package path RUN printf '%s\n' \ diff --git a/_typos.toml b/_typos.toml index 189626b2..c601cab8 100644 --- a/_typos.toml +++ b/_typos.toml @@ -6,12 +6,12 @@ unparseable = "unparseable" [files] extend-exclude = [ - "*.lock", - "target/", - "*.pyc", - "__pycache__/", - "python/src/gen/", - "rust/src/gen/", - "pheno-cli/go.mod", - "templates/static/*.min.js", + "*.lock", + "target/", + "*.pyc", + "__pycache__/", + "python/src/gen/", + "rust/src/gen/", + "pheno-cli/go.mod", + "templates/static/*.min.js", ] diff --git a/audit.toml b/audit.toml index 62584c85..abc9d0b2 100644 --- a/audit.toml +++ b/audit.toml @@ -3,11 +3,11 @@ # CI runs `cargo audit` without `--deny warnings` so real vulns still fail. [advisories] ignore = [ - # Marvin attack advisory covers every released version of rsa (introduced 0.0.0-0, no patch). - # sharecli only decodes/verifies JWTs with RSA public keys for local sidecar auth (src/serve_auth.rs); - # it never performs network-exposed RSA private-key operations. - "RUSTSEC-2023-0071", - # Informational 'unmaintained' advisory for paste - stale Cargo.lock entry, not in the workspace dep graph. - "RUSTSEC-2024-0436", + # Marvin attack advisory covers every released version of rsa (introduced 0.0.0-0, no patch). + # sharecli only decodes/verifies JWTs with RSA public keys for local sidecar auth (src/serve_auth.rs); + # it never performs network-exposed RSA private-key operations. + "RUSTSEC-2023-0071", + # Informational 'unmaintained' advisory for paste - stale Cargo.lock entry, not in the workspace dep graph. + "RUSTSEC-2024-0436", ] yanked = "warn" diff --git a/cliff.toml b/cliff.toml index cd7e342f..dba4b787 100644 --- a/cliff.toml +++ b/cliff.toml @@ -33,19 +33,17 @@ trim = true conventional_commits = true commit_parsers = [ - {message = "^feat", group = "Features"}, - {message = "^fix", group = "Bug Fixes"}, - {message = "^doc", group = "Documentation"}, - {message = "^perf", group = "Performance"}, - {message = "^refactor", group = "Refactoring"}, - {message = "^test", group = "Testing"}, - {message = "^chore", skip = true}, - {message = "^ci", skip = true}, + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^test", group = "Testing" }, + { message = "^chore", skip = true }, + { message = "^ci", skip = true }, ] -commit_filters = [ - {remove_if_matches = ".*skip.*changelog.*"}, -] +commit_filters = [{ remove_if_matches = ".*skip.*changelog.*" }] tag_pattern = "v[0-9].*" skip_tags = "" diff --git a/codecov.yml b/codecov.yml index 6e5b7ebd..a60a9cfc 100644 --- a/codecov.yml +++ b/codecov.yml @@ -7,9 +7,7 @@ coverage: paths: - "src/" - "sharecli/" - - - "src/**/*.rs" - + - "src/**/*.rs" patch: default: target: 80% @@ -30,8 +28,6 @@ ignore: - ".github/" - "examples/" - "scripts/" - - flags: unit: diff --git a/crates/sharecli-fuse/src/backend.rs b/crates/sharecli-fuse/src/backend.rs index ce2fd829..f3b4339a 100644 --- a/crates/sharecli-fuse/src/backend.rs +++ b/crates/sharecli-fuse/src/backend.rs @@ -1,23 +1,22 @@ -//! Runtime backend negotiation for macFUSE on macOS. - +/// Runtime backend negotiation for macFUSE on macOS. +/// +/// The backend only matters on macOS: macOS 15+ prefers the File System Kit +/// backend while older releases rely on the kernel extension. On Linux and +/// Windows the FUSE layer is platform-native (libfuse3 / WinFsp) and the +/// backend is always unavailable. use std::{ path::{Path, PathBuf}, process::Command, }; -/// Available FUSE backend options on macOS. -/// -/// Selected at runtime by [`select_backend`]; the chosen variant is what -/// `InterceptFs::mount` will negotiate with the host kernel. +/// macOS FUSE backend selected at mount time (macFUSE only). #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum FuseBackend { - /// Apple's first-party FSKit user-space file system framework (macOS 15+). + /// File System Kit backend (macOS 15+, preferred when available). Fskit, - /// The legacy macFUSE kext (`/Library/Filesystems/macfuse.fs`) — used when - /// the kext is already loaded because it offers the lowest-latency path. + /// Legacy macFUSE kernel-extension backend. Kernel, - /// No backend is available; mount negotiation will fail with a - /// diagnostic from [`runtime_diagnostics`]. + /// No usable backend detected (or non-macOS platform). Unavailable, } @@ -231,6 +230,11 @@ fn kernel_backend_loaded() -> bool { /// /// This is intentionally executed only on an error path by callers. It does not /// alter backend selection and avoids shelling out through a user-controlled shell. +/// +/// Only reachable from `#[cfg(target_os = "macos")]` mount code; on other +/// platforms the body is a no-op string, so keep the function compiled for the +/// cross-platform diagnostics path but silence the dead-code lint there. +#[cfg_attr(not(target_os = "macos"), allow(dead_code))] pub(crate) fn runtime_diagnostics() -> String { #[cfg(target_os = "macos")] { @@ -270,7 +274,7 @@ pub(crate) fn runtime_diagnostics() -> String { } }) .unwrap_or("unavailable"); - format!("macFUSE version-entry={version}; {kext}; fskit_agent={fskit}") + return format!("macFUSE version-entry={version}; {kext}; fskit_agent={fskit}"); } #[cfg(not(target_os = "macos"))] { @@ -278,6 +282,10 @@ pub(crate) fn runtime_diagnostics() -> String { } } +/// Extract the `CFBundleShortVersionString` value from a macFUSE version plist. +/// +/// Only called from [`runtime_diagnostics`], which is macOS-only at runtime. +#[cfg_attr(not(target_os = "macos"), allow(dead_code))] fn parse_bundle_version(contents: &str) -> Option { let mut lines = contents.lines(); while let Some(line) = lines.next() { diff --git a/crates/sharecli-fuse/src/provenance.rs b/crates/sharecli-fuse/src/provenance.rs index 59a1fee9..741e8fcd 100644 --- a/crates/sharecli-fuse/src/provenance.rs +++ b/crates/sharecli-fuse/src/provenance.rs @@ -14,6 +14,8 @@ //! - [`ATTR_WRITTEN_AT`] — Unix epoch seconds as decimal ASCII use std::path::Path; +#[cfg(windows)] +use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; /// Extended-attribute name for the writer session id. diff --git a/crates/sharecli-fuse/src/session_registry.rs b/crates/sharecli-fuse/src/session_registry.rs index b6c2dba5..9112fe0a 100644 --- a/crates/sharecli-fuse/src/session_registry.rs +++ b/crates/sharecli-fuse/src/session_registry.rs @@ -56,18 +56,18 @@ pub fn smoke_fuser_config() -> Config { /// FUSE config for privileged mount smoke / ephemeral mounts with an explicit backend override. #[cfg(any(target_os = "linux", target_os = "macos"))] -pub fn smoke_fuser_config_for_backend(backend: Option) -> Config { +pub fn smoke_fuser_config_for_backend(_backend: Option) -> Config { #[cfg(target_os = "linux")] { let mut config = Config::default(); config.mount_options = vec![MountOption::FSName("sharecli-fuse-smoke".to_string())]; config.acl = SessionACL::RootAndOwner; - return config; + config } #[cfg(target_os = "macos")] { - let _ = backend; + let _ = _backend; let mut config = Config::default(); config.mount_options = vec![MountOption::FSName("sharecli-fuse-smoke".to_string())]; // macFUSE's mount helper has no backend= option. Backend negotiation is diff --git a/crates/sharecli-session/Cargo.toml b/crates/sharecli-session/Cargo.toml index e23f6957..92a0905b 100644 --- a/crates/sharecli-session/Cargo.toml +++ b/crates/sharecli-session/Cargo.toml @@ -10,4 +10,11 @@ base64 = "0.22" rusqlite = { version = "0.40", features = ["bundled"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -tokio = { version = "1", features = ["net", "io-util", "fs", "rt", "macros", "time"] } +tokio = { version = "1", features = [ + "net", + "io-util", + "fs", + "rt", + "macros", + "time", +] } diff --git a/crates/sharecli-session/tests/session_ledger.rs b/crates/sharecli-session/tests/session_ledger.rs index b21c7a9c..ee77dd4d 100644 --- a/crates/sharecli-session/tests/session_ledger.rs +++ b/crates/sharecli-session/tests/session_ledger.rs @@ -1,13 +1,25 @@ -use sharecli_session::{ResolutionConfidence, SessionObservation, SessionStore}; +use sharecli_session::{ + AgentSession, ObservationKind, ResolutionConfidence, SessionObservation, SessionStore, + SurfaceCapabilities, SurfaceRecord, +}; +use std::path::PathBuf; fn observation(id: &str, session_id: &str, confidence: ResolutionConfidence) -> SessionObservation { + let surface = SurfaceRecord { + id: id.to_string(), + terminal: "ghostty".to_string(), + title: None, + cwd: PathBuf::from("/tmp"), + process: None, + }; + let mut session = AgentSession::new("codex", session_id, "/tmp"); + session.confidence = confidence; SessionObservation::new( - id, - session_id, - "surface-1", "2026-08-08T00:00:00Z", - confidence, - "terminal process and harness metadata", + surface, + Some(session), + SurfaceCapabilities::default(), + ObservationKind::Updated, ) } @@ -27,10 +39,11 @@ fn observations_survive_store_reopen() { } let reopened = SessionStore::open(&path).unwrap(); - let rows = reopened.observations("codex:abc").unwrap(); + let rows = reopened.observations(None).unwrap(); assert_eq!(rows.len(), 1); - assert!(rows[0].resumable); - assert_eq!(rows[0].confidence, ResolutionConfidence::Exact); + let session = rows[0].session.as_ref().expect("observation carries session"); + assert!(session.auto_resumable(), "Exact-confidence session must be auto-resumable"); + assert_eq!(session.confidence, ResolutionConfidence::Exact); std::fs::remove_file(&path).unwrap(); } @@ -46,8 +59,9 @@ fn heuristic_observations_are_persisted_but_not_resumable() { )) .unwrap(); - let rows = store.observations("codex:ambiguous").unwrap(); + let rows = store.observations(None).unwrap(); assert_eq!(rows.len(), 1); - assert!(!rows[0].resumable); - assert_eq!(rows[0].confidence, ResolutionConfidence::Heuristic); + let session = rows[0].session.as_ref().expect("observation carries session"); + assert!(!session.auto_resumable(), "Heuristic-confidence session must not be auto-resumable"); + assert_eq!(session.confidence, ResolutionConfidence::Heuristic); } diff --git a/crates/sharecli-sync/src/lib.rs b/crates/sharecli-sync/src/lib.rs index 784935d5..110f4c3f 100644 --- a/crates/sharecli-sync/src/lib.rs +++ b/crates/sharecli-sync/src/lib.rs @@ -22,22 +22,27 @@ impl PoolIndex { /// Insert `pid` when absent. Returns `true` when inserted. pub fn insert(&self, pid: u32, name: impl Into) -> bool { - self.names.write().unwrap().insert(pid, name.into()).is_none() + self.names + .write() + .expect("pool index write lock poisoned") + .insert(pid, name.into()) + .is_none() } /// Remove `pid` when present. Returns `true` when removed. pub fn remove(&self, pid: u32) -> bool { - self.names.write().unwrap().remove(&pid).is_some() + self.names.write().expect("pool index write lock poisoned").remove(&pid).is_some() } /// Number of tracked pids. pub fn count(&self) -> usize { - self.names.read().unwrap().len() + self.names.read().expect("pool index read lock poisoned").len() } /// Snapshot of tracked pids (sorted for deterministic assertions). pub fn pids_sorted(&self) -> Vec { - let mut pids: Vec = self.names.read().unwrap().keys().copied().collect(); + let mut pids: Vec = + self.names.read().expect("pool index read lock poisoned").keys().copied().collect(); pids.sort_unstable(); pids } diff --git a/deny.toml b/deny.toml index ce945481..1bb60ef9 100644 --- a/deny.toml +++ b/deny.toml @@ -8,29 +8,29 @@ [licenses] version = 2 allow = [ - "Apache-2.0", - "Apache-2.0 WITH LLVM-exception", - "BSD-2-Clause", - "BSD-3-Clause", - "BSD-3-Clause-Clear", - "CC0-1.0", - "CC-BY-SA-4.0", - "CDDL-1.0", - "GPL-3.0-only", - # winfsp / winfsp-sys (Windows FUSE bindings, sharecli-fuse) declare the - # deprecated SPDX id "GPL-3.0"; cargo-deny >= 0.18.4 matches it pedantically. - "GPL-3.0", - "ISC", - "MIT", - "MPL-2.0", - "Unicode-3.0", - "Unicode-DFS-2016", - "Zlib", - "0BSD", - "BlueOak-1.0.0", - "CDLA-Permissive-2.0", - "Unlicense", - "WTFPL", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "BSD-3-Clause-Clear", + "CC0-1.0", + "CC-BY-SA-4.0", + "CDDL-1.0", + "GPL-3.0-only", + # winfsp / winfsp-sys (Windows FUSE bindings, sharecli-fuse) declare the + # deprecated SPDX id "GPL-3.0"; cargo-deny >= 0.18.4 matches it pedantically. + "GPL-3.0", + "ISC", + "MIT", + "MPL-2.0", + "Unicode-3.0", + "Unicode-DFS-2016", + "Zlib", + "0BSD", + "BlueOak-1.0.0", + "CDLA-Permissive-2.0", + "Unlicense", + "WTFPL", ] [sources] @@ -41,16 +41,16 @@ allow-git = ["https://github.com/KooshaPari/substrate"] [advisories] ignore = [ - # No safe upgrade available - rustls-pemfile deprecated, migration requires async-nats update - { id = "RUSTSEC-2025-0134", reason = "No safe upgrade available. rustls-pemfile deprecated, migration requires async-nats update." }, - # No safe upgrade available - gix = "0.71" pinned in Cargo.toml, requires major version bump - { id = "RUSTSEC-2025-0140", reason = "No safe upgrade available. gix = 0.71 pinned in Cargo.toml, requires breaking API change." }, - # No safe upgrade available - async-nats 0.46.0 still uses rustls-webpki 0.102.x - { id = "RUSTSEC-2026-0049", reason = "No safe upgrade available. async-nats 0.46.0 still uses vulnerable rustls-webpki." }, - # Marvin attack advisory covers every released version of rsa (introduced 0.0.0-0, no patch). - # sharecli only decodes/verifies JWTs with RSA public keys for local sidecar auth (src/serve_auth.rs); - # it never performs network-exposed RSA private-key operations. - { id = "RUSTSEC-2023-0071", reason = "No patched rsa release exists (advisory covers all versions); JWT use is local public-key verification only." }, - # Informational 'unmaintained' advisory for paste - stale Cargo.lock entry, not in the workspace dep graph. - { id = "RUSTSEC-2024-0436", reason = "paste is a stale Cargo.lock entry; cargo tree -i paste is empty, nothing to replace." }, + # No safe upgrade available - rustls-pemfile deprecated, migration requires async-nats update + { id = "RUSTSEC-2025-0134", reason = "No safe upgrade available. rustls-pemfile deprecated, migration requires async-nats update." }, + # No safe upgrade available - gix = "0.71" pinned in Cargo.toml, requires major version bump + { id = "RUSTSEC-2025-0140", reason = "No safe upgrade available. gix = 0.71 pinned in Cargo.toml, requires breaking API change." }, + # No safe upgrade available - async-nats 0.46.0 still uses rustls-webpki 0.102.x + { id = "RUSTSEC-2026-0049", reason = "No safe upgrade available. async-nats 0.46.0 still uses vulnerable rustls-webpki." }, + # Marvin attack advisory covers every released version of rsa (introduced 0.0.0-0, no patch). + # sharecli only decodes/verifies JWTs with RSA public keys for local sidecar auth (src/serve_auth.rs); + # it never performs network-exposed RSA private-key operations. + { id = "RUSTSEC-2023-0071", reason = "No patched rsa release exists (advisory covers all versions); JWT use is local public-key verification only." }, + # Informational 'unmaintained' advisory for paste - stale Cargo.lock entry, not in the workspace dep graph. + { id = "RUSTSEC-2024-0436", reason = "paste is a stale Cargo.lock entry; cargo tree -i paste is empty, nothing to replace." }, ] diff --git a/gitleaks.toml b/gitleaks.toml index 247467bb..b224300a 100644 --- a/gitleaks.toml +++ b/gitleaks.toml @@ -3,128 +3,127 @@ # Auto-generated by pheno-cli bootstrap [allowlist] - description = "Public placeholders and examples" - files = [ - "^\\.env\\.example$", - "^\\.env\\.template$", - "^\\.env\\.local\\.example$", - "^docs/.*$", - "^\\.github/.*$", - "^.*\\.md$", - "^test.*$", - "^.*_test\\.py$", - "^.*_test\\.go$", - "^tests/.*$" - ] - - # Allow known test/dev placeholder patterns - regexes = [ - "^agileplus-dev$", - "^dev-secret-key-change-in-prod$", - "^your-.+-here$", - "^CHANGE_ME_IN_PROD$", - "^PLACEHOLDER_", - "^test-secret$", - "^example-key$", - "^dummy-token$", - "^fake-api-key$" - ] +description = "Public placeholders and examples" +files = [ + "^\\.env\\.example$", + "^\\.env\\.template$", + "^\\.env\\.local\\.example$", + "^docs/.*$", + "^\\.github/.*$", + "^.*\\.md$", + "^test.*$", + "^.*_test\\.py$", + "^.*_test\\.go$", + "^tests/.*$", +] + +# Allow known test/dev placeholder patterns +regexes = [ + "^agileplus-dev$", + "^dev-secret-key-change-in-prod$", + "^your-.+-here$", + "^CHANGE_ME_IN_PROD$", + "^PLACEHOLDER_", + "^test-secret$", + "^example-key$", + "^dummy-token$", + "^fake-api-key$", +] [[rules]] - id = "openrouter-api-key" - description = "OpenRouter API Key" - regex = '''sk-or-v1-[a-f0-9]{32,}''' - tags = ["apikey", "openrouter"] - entropy = 4.5 +id = "openrouter-api-key" +description = "OpenRouter API Key" +regex = '''sk-or-v1-[a-f0-9]{32,}''' +tags = ["apikey", "openrouter"] +entropy = 4.5 [[rules]] - id = "openai-api-key" - description = "OpenAI API Key" - regex = '''sk-[a-zA-Z0-9]{48}''' - tags = ["apikey", "openai"] - entropy = 4.5 +id = "openai-api-key" +description = "OpenAI API Key" +regex = '''sk-[a-zA-Z0-9]{48}''' +tags = ["apikey", "openai"] +entropy = 4.5 [[rules]] - id = "anthropic-api-key" - description = "Anthropic API Key" - regex = '''sk-ant-[a-zA-Z0-9]{32,}''' - tags = ["apikey", "anthropic"] - entropy = 4.5 +id = "anthropic-api-key" +description = "Anthropic API Key" +regex = '''sk-ant-[a-zA-Z0-9]{32,}''' +tags = ["apikey", "anthropic"] +entropy = 4.5 [[rules]] - id = "github-oauth-token" - description = "GitHub OAuth Token" - regex = '''ghp_[a-zA-Z0-9]{36,}''' - tags = ["apikey", "github"] - entropy = 4.5 +id = "github-oauth-token" +description = "GitHub OAuth Token" +regex = '''ghp_[a-zA-Z0-9]{36,}''' +tags = ["apikey", "github"] +entropy = 4.5 [[rules]] - id = "github-fine-grained-pat" - description = "GitHub Fine-Grained PAT" - regex = '''github_pat_[a-zA-Z0-9_]{22,}''' - tags = ["apikey", "github"] - entropy = 4.5 +id = "github-fine-grained-pat" +description = "GitHub Fine-Grained PAT" +regex = '''github_pat_[a-zA-Z0-9_]{22,}''' +tags = ["apikey", "github"] +entropy = 4.5 [[rules]] - id = "github-app-token" - description = "GitHub App Token" - regex = '''ghs_[a-zA-Z0-9]{36}''' - tags = ["apikey", "github"] - entropy = 4.5 +id = "github-app-token" +description = "GitHub App Token" +regex = '''ghs_[a-zA-Z0-9]{36}''' +tags = ["apikey", "github"] +entropy = 4.5 [[rules]] - id = "generic-api-key" - description = "Generic API Key" - regex = '''(?i)(api[_-]?key|apikey|api[_-]?secret)[\s]*[=:]+[\s]*['"]?[a-zA-Z0-9_-]{20,}['"]?''' - tags = ["apikey", "generic"] - entropy = 4.0 +id = "generic-api-key" +description = "Generic API Key" +regex = '''(?i)(api[_-]?key|apikey|api[_-]?secret)[\s]*[=:]+[\s]*['"]?[a-zA-Z0-9_-]{20,}['"]?''' +tags = ["apikey", "generic"] +entropy = 4.0 [[rules]] - id = "aws-access-key" - description = "AWS Access Key" - regex = '''AKIA[0-9A-Z]{16}''' - tags = ["aws", "apikey"] - entropy = 4.0 +id = "aws-access-key" +description = "AWS Access Key" +regex = '''AKIA[0-9A-Z]{16}''' +tags = ["aws", "apikey"] +entropy = 4.0 [[rules]] - id = "aws-secret-key" - description = "AWS Secret Key" - regex = '''[0-9a-zA-Z/+]{40}''' - tags = ["aws", "secret"] - entropy = 4.5 - secretGroup = 1 +id = "aws-secret-key" +description = "AWS Secret Key" +regex = '''[0-9a-zA-Z/+]{40}''' +tags = ["aws", "secret"] +entropy = 4.5 +secretGroup = 1 [[rules]] - id = "slack-token" - description = "Slack Token" - regex = '''xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*''' - tags = ["slack", "token"] - entropy = 4.0 +id = "slack-token" +description = "Slack Token" +regex = '''xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*''' +tags = ["slack", "token"] +entropy = 4.0 [[rules]] - id = "slack-webhook" - description = "Slack Webhook" - regex = '''https://hooks\.slack\.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,}/[a-zA-Z0-9_]{24}''' - tags = ["slack", "webhook"] +id = "slack-webhook" +description = "Slack Webhook" +regex = '''https://hooks\.slack\.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,}/[a-zA-Z0-9_]{24}''' +tags = ["slack", "webhook"] [[rules]] - id = "private-key" - description = "Private Key" - regex = '''-----BEGIN (RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----''' - tags = ["privatekey", "crypto"] +id = "private-key" +description = "Private Key" +regex = '''-----BEGIN (RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----''' +tags = ["privatekey", "crypto"] [[rules]] - id = "jwt-token" - description = "JWT Token" - regex = '''eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*''' - tags = ["jwt", "token"] - entropy = 4.0 - +id = "jwt-token" +description = "JWT Token" +regex = '''eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*''' +tags = ["jwt", "token"] +entropy = 4.0 [[rules]] - id = "database-url" - description = "Database URL with credentials" - regex = '''(?i)(postgres|mysql|mongodb)://[^:]+:[^@]+@''' - tags = ["database", "connection"] - entropy = 4.0 +id = "database-url" +description = "Database URL with credentials" +regex = '''(?i)(postgres|mysql|mongodb)://[^:]+:[^@]+@''' +tags = ["database", "connection"] +entropy = 4.0 diff --git a/lib/teamcomm/Cargo.toml b/lib/teamcomm/Cargo.toml index 92427a7d..ef068689 100644 --- a/lib/teamcomm/Cargo.toml +++ b/lib/teamcomm/Cargo.toml @@ -1,12 +1,12 @@ [workspace] resolver = "2" members = [ - "crates/teamcomm-protocol", - "crates/teamcomm-client", - "crates/teamcomm-daemon", - "crates/teamcomm-cli", - "crates/teamcomm-mcp", - "crates/teamcomm-smoke", + "crates/teamcomm-protocol", + "crates/teamcomm-client", + "crates/teamcomm-daemon", + "crates/teamcomm-cli", + "crates/teamcomm-mcp", + "crates/teamcomm-smoke", ] [workspace.package] diff --git a/lib/teamcomm/crates/teamcomm-cli/Cargo.toml b/lib/teamcomm/crates/teamcomm-cli/Cargo.toml index 2f8827f0..cb569e98 100644 --- a/lib/teamcomm/crates/teamcomm-cli/Cargo.toml +++ b/lib/teamcomm/crates/teamcomm-cli/Cargo.toml @@ -20,7 +20,13 @@ teamcomm-client = { path = "../teamcomm-client" } clap = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util", "macros", "time"] } +tokio = { version = "1", features = [ + "rt-multi-thread", + "net", + "io-util", + "macros", + "time", +] } anyhow = { workspace = true } comfy-table = { workspace = true } tracing = { workspace = true } diff --git a/lib/teamcomm/crates/teamcomm-client/Cargo.toml b/lib/teamcomm/crates/teamcomm-client/Cargo.toml index 342ddb0b..04fc0183 100644 --- a/lib/teamcomm/crates/teamcomm-client/Cargo.toml +++ b/lib/teamcomm/crates/teamcomm-client/Cargo.toml @@ -14,7 +14,12 @@ authors = ["kooshapari"] teamcomm-protocol = { path = "../teamcomm-protocol" } serde = { workspace = true } serde_json = { workspace = true } -tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util", "macros"] } +tokio = { version = "1", features = [ + "rt-multi-thread", + "net", + "io-util", + "macros", +] } anyhow = { workspace = true } tracing = { workspace = true } dirs = { workspace = true } diff --git a/mise.toml b/mise.toml index e227bfb2..26e631a0 100644 --- a/mise.toml +++ b/mise.toml @@ -34,8 +34,7 @@ description = "OSV/GHSA lockfile scan (HIGH/CRITICAL hard parity)" run = "bash scripts/ci/osv_scan.sh" -[tasks.docs:build] +["tasks.docs:build"] description = "Build documentation" run = "cargo doc --no-deps --open" - diff --git a/mutants.toml b/mutants.toml index 41da304e..636c09bf 100644 --- a/mutants.toml +++ b/mutants.toml @@ -3,15 +3,8 @@ # Threshold contract: docs/ops/mutants-threshold.md # Schema: cargo-mutants ≥25 (examine_globs + exclude_re; no jobs/timeout keys) -examine_globs = [ - "crates/sharecli-thermal-tui/src/lib.rs", -] +examine_globs = ["crates/sharecli-thermal-tui/src/lib.rs"] -exclude_re = [ - "render", - "event_loop", - "replace run ", - "count_cargo_builds", -] +exclude_re = ["render", "event_loop", "replace run ", "count_cargo_builds"] test_package = ["sharecli-thermal-tui"] diff --git a/src/alloc.rs b/src/alloc.rs index ca788ac1..54d9755c 100644 --- a/src/alloc.rs +++ b/src/alloc.rs @@ -14,7 +14,9 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; static DHAT: dhat::Alloc = dhat::Alloc; /// Returns the active allocator label for operator diagnostics (`sharecli --version` path). -#[allow(dead_code)] // consumed by the lib target; bin target doesn't use it yet +/// Exercised by the unit test below; the bin target's dead-code pass cannot +/// see test-only callers, so allow it outside `cfg(test)` builds. +#[cfg_attr(not(test), allow(dead_code))] pub fn active_allocator_label() -> &'static str { if cfg!(feature = "dhat-heap") { "dhat" diff --git a/src/base_n_radix.rs b/src/base_n_radix.rs index 3f89dd4b..3d8f301f 100644 --- a/src/base_n_radix.rs +++ b/src/base_n_radix.rs @@ -47,7 +47,7 @@ pub fn big_encode(value: &[u32], base: u32) -> String { } // Process limbs high-to-low (big-endian). let mut limbs: Vec = value.to_vec(); - while !limbs.is_empty() && *limbs.last().unwrap() == 0 { + while !limbs.is_empty() && *limbs.last().expect("limbs non-empty (guard checked above)") == 0 { limbs.pop(); } let mut out = String::new(); @@ -60,7 +60,9 @@ pub fn big_encode(value: &[u32], base: u32) -> String { } let c = if carry < 10 { b'0' + carry as u8 } else { b'a' + (carry - 10) as u8 }; out.push(c as char); - while !limbs.is_empty() && *limbs.last().unwrap() == 0 { + while !limbs.is_empty() + && *limbs.last().expect("limbs non-empty (guard checked above)") == 0 + { limbs.pop(); } } diff --git a/src/cast/caster.rs b/src/cast/caster.rs index 013b3ccb..dff564d4 100644 --- a/src/cast/caster.rs +++ b/src/cast/caster.rs @@ -95,7 +95,7 @@ impl ProcessRunner for SystemRunner { .stderr(std::process::Stdio::piped()) .spawn()?; use std::io::Write; - child.stdin.take().unwrap().write_all(stdin)?; + child.stdin.take().expect("spawned child must have piped stdin").write_all(stdin)?; child.wait_with_output() } } @@ -126,13 +126,15 @@ impl MockProcessRunner { pub fn push_ok(&mut self, bin: &str, args: &[&str]) { self.commands .lock() - .unwrap() + .expect("MockProcessRunner commands mutex poisoned") .push_back((bin.to_string(), args.iter().map(|s| s.to_string()).collect())); - self.outputs.lock().unwrap().push_back(Ok(std::process::Output { - status: std::process::ExitStatus::default(), - stdout: Vec::new(), - stderr: Vec::new(), - })); + self.outputs.lock().expect("MockProcessRunner outputs mutex poisoned").push_back(Ok( + std::process::Output { + status: std::process::ExitStatus::default(), + stdout: Vec::new(), + stderr: Vec::new(), + }, + )); } /// Create a runner pre-loaded with commands that all succeed. @@ -153,10 +155,11 @@ impl MockProcessRunner { for (bin, args) in cmds { r.commands .lock() - .unwrap() + .expect("MockProcessRunner commands mutex poisoned") .push_back((bin.to_string(), args.iter().map(|s| s.to_string()).collect())); } - *r.outputs.lock().unwrap() = std::collections::VecDeque::from(outputs); + *r.outputs.lock().expect("MockProcessRunner outputs mutex poisoned") = + std::collections::VecDeque::from(outputs); r } } @@ -166,7 +169,7 @@ impl ProcessRunner for MockProcessRunner { let expected = self .commands .lock() - .unwrap() + .expect("MockProcessRunner commands mutex poisoned") .pop_front() .expect("MockProcessRunner: no more commands expected"); assert_eq!(expected.0, bin, "MockProcessRunner: bin mismatch"); @@ -176,13 +179,17 @@ impl ProcessRunner for MockProcessRunner { "MockProcessRunner: args mismatch for bin {}", bin ); - self.outputs.lock().unwrap().pop_front().unwrap_or_else(|| { - Ok(std::process::Output { - status: std::process::ExitStatus::default(), - stdout: Vec::new(), - stderr: Vec::new(), + self.outputs + .lock() + .expect("MockProcessRunner outputs mutex poisoned") + .pop_front() + .unwrap_or_else(|| { + Ok(std::process::Output { + status: std::process::ExitStatus::default(), + stdout: Vec::new(), + stderr: Vec::new(), + }) }) - }) } fn run_with_stdin( diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 7d454564..c46b4366 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -20,10 +20,10 @@ use crate::monitoring::HostResourceWatchJson; use crate::config::{self, Config, ConfigCmd, ProjectCmd}; use crate::progress::StepProgress; -#[allow(unused_imports)] // ProcState used by tests via `use super::*;` +#[cfg(test)] +use crate::runtime::ProcState; use crate::runtime::{ - ProcState, ProcessFilter, ProcessInfo, ProcessPool, ProjectLimits, ProjectResources, - SharedRuntime, + ProcessFilter, ProcessInfo, ProcessPool, ProjectLimits, ProjectResources, SharedRuntime, }; use crate::spawn_policy::SpawnPolicy; use sharecli_fleet::global_coalesce_meters; diff --git a/src/commands/proc.rs b/src/commands/proc.rs index 3e4d1520..0f66710d 100644 --- a/src/commands/proc.rs +++ b/src/commands/proc.rs @@ -63,7 +63,7 @@ pub fn parse_proc_state(raw: &str) -> Result { if trimmed.len() != 1 { bail!("invalid --state value '{raw}'; expected single process state letter (R|S|D|Z|T|…)"); } - let ch = trimmed.chars().next().unwrap(); + let ch = trimmed.chars().next().expect("single-char --state value (length validated above)"); let normalized = match ch { 'r' | 'R' => 'R', 's' | 'S' => 'S', @@ -89,6 +89,9 @@ pub fn parse_fd_count(raw: &str, flag: &str) -> Result { } impl ProcFilter { + // CLI flag aggregation: every `proc` inventory filter is threaded through as + // an individual Option so the dispatch layer stays mechanical. The wide + // signature is intentional (mirrors `reject_pid_inventory_combos` / `run`). #[allow(clippy::too_many_arguments)] pub fn from_cli( family: Option, @@ -1364,6 +1367,8 @@ pub async fn render_once( /// `sharecli proc` — list host-detected agents with live RSS/FD samples. /// Reject inventory-mode flags when `--pid` selects detail mode (AC-007.92). +// CLI flag aggregation: one Option per inventory filter keeps the dispatch +// layer mechanical; the wide signature is intentional. #[allow(clippy::too_many_arguments)] fn reject_pid_inventory_combos( tree: bool, @@ -1425,6 +1430,8 @@ fn reject_pid_inventory_combos( ) } +// CLI entry point: all `proc` flags are threaded through as individual +// arguments by the shared dispatch layer; the wide signature is intentional. #[allow(clippy::too_many_arguments)] pub async fn run( json: bool, diff --git a/src/commands/report.rs b/src/commands/report.rs index 850cbd54..f5bad2ec 100644 --- a/src/commands/report.rs +++ b/src/commands/report.rs @@ -15,8 +15,9 @@ use sharecli_fleet::GateStatusSnapshot; use crate::commands::{PoolJson, StatusJson}; use crate::monitoring::HostResourceWatchJson; -#[allow(unused_imports)] // ProcState used by tests via `use super::*;` -use crate::runtime::{ProcState, ProcessInfo, ProcessPool}; +#[cfg(test)] +use crate::runtime::ProcState; +use crate::runtime::{ProcessInfo, ProcessPool}; // --------------------------------------------------------------------------- // Public types diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 5cfab941..331ed8a4 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -42,8 +42,9 @@ use crate::health_check::{HealthCheckScheduler, HealthCheckStore}; use crate::http_red::{render_http_red_metrics, HttpRedMetrics}; use crate::monitoring::HostResourceWatchJson; use crate::notifier::Notifier; -#[allow(unused_imports)] // ProcState used by tests via `use super::*;` -use crate::runtime::{ProcState, ProcessPool}; +#[cfg(test)] +use crate::runtime::ProcState; +use crate::runtime::ProcessPool; use crate::serve_auth::{self, ServeAuth}; use crate::serve_lock::{decide, probe, Decision, OnConflict, ServeState}; use crate::serve_rate_limit::{is_probe_path, ServeRateLimit, ServeRateLimitState}; diff --git a/src/dns_zone.rs b/src/dns_zone.rs index e287979c..75d607ab 100644 --- a/src/dns_zone.rs +++ b/src/dns_zone.rs @@ -133,8 +133,11 @@ pub fn parse(input: &str) -> Result { let name_owned; if is_type_token(name_token) { // No explicit owner — treat as inherited. - name_owned = - String::from(if records.is_empty() { "@" } else { &records.last().unwrap().name }); + name_owned = String::from(if records.is_empty() { + "@" + } else { + &records.last().expect("records non-empty in else branch").name + }); // Don't consume the token; it's the type. } else { name_owned = name_token.to_string(); diff --git a/src/hkdf.rs b/src/hkdf.rs index 8669f1eb..610ccdd5 100644 --- a/src/hkdf.rs +++ b/src/hkdf.rs @@ -184,9 +184,7 @@ pub fn extract(salt: &[u8], ikm: &[u8]) -> [u8; SHA256_OUT] { /// length (capped at 255 * HashLen per the spec). pub fn expand(prk: &[u8; SHA256_OUT], info: &[u8], length: usize) -> Vec { let max = 255 * SHA256_OUT; - if length > max { - panic!("HKDF-Expand: requested {} bytes exceeds max {}", length, max); - } + assert!(length <= max, "HKDF-Expand: requested {} bytes exceeds max {}", length, max); let mut okm = Vec::with_capacity(length); let mut t = Vec::::new(); let mut counter: u8 = 1; diff --git a/src/ipaddr_validation.rs b/src/ipaddr_validation.rs index c500d80a..c14a35f0 100644 --- a/src/ipaddr_validation.rs +++ b/src/ipaddr_validation.rs @@ -31,7 +31,7 @@ pub fn parse_ipv4(s: &str) -> Option<[u8; 4]> { } let mut out = [0u8; 4]; for (i, p) in s.split('.').enumerate() { - out[i] = p.parse().unwrap(); + out[i] = p.parse().expect("is_ipv4 validated each octet is 0-255"); } Some(out) } diff --git a/src/itoa.rs b/src/itoa.rs index c01d0878..dff3b7ad 100644 --- a/src/itoa.rs +++ b/src/itoa.rs @@ -13,7 +13,7 @@ pub fn u32_to_str(n: u32) -> String { i += 1; } buf[..i].reverse(); - std::str::from_utf8(&buf[..i]).unwrap().to_string() + std::str::from_utf8(&buf[..i]).expect("digit buffer is pure ASCII").to_string() } pub fn i32_to_str(n: i32) -> String { @@ -39,7 +39,7 @@ pub fn u64_to_str(n: u64) -> String { i += 1; } buf[..i].reverse(); - std::str::from_utf8(&buf[..i]).unwrap().to_string() + std::str::from_utf8(&buf[..i]).expect("digit buffer is pure ASCII").to_string() } #[cfg(test)] mod tests { diff --git a/src/log_sink.rs b/src/log_sink.rs index 8648cef5..f60e5b44 100644 --- a/src/log_sink.rs +++ b/src/log_sink.rs @@ -25,7 +25,7 @@ impl LogSink { Self { buffer: Arc::new(Mutex::new(VecDeque::new())), capacity } } pub fn write(&self, level: LogLevel, msg: impl Into) { - let mut buf = self.buffer.lock().unwrap(); + let mut buf = self.buffer.lock().expect("LogSink buffer mutex poisoned"); if buf.len() >= self.capacity { buf.pop_front(); } @@ -41,10 +41,10 @@ impl LogSink { self.write(LogLevel::Error, msg); } pub fn drain(&self) -> Vec { - self.buffer.lock().unwrap().drain(..).collect() + self.buffer.lock().expect("LogSink buffer mutex poisoned").drain(..).collect() } pub fn len(&self) -> usize { - self.buffer.lock().unwrap().len() + self.buffer.lock().expect("LogSink buffer mutex poisoned").len() } pub fn is_empty(&self) -> bool { self.len() == 0 diff --git a/src/main.rs b/src/main.rs index aa424d27..77603144 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1664,7 +1664,10 @@ async fn prune(idle_seconds: u64, force: bool) -> Result<()> { sys.refresh_all(); let processes = pool.list().await; - let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock before Unix epoch") + .as_secs(); let candidates: Vec<_> = processes .into_iter() diff --git a/src/mapi_props.rs b/src/mapi_props.rs index 2e30181e..81e7792c 100644 --- a/src/mapi_props.rs +++ b/src/mapi_props.rs @@ -167,14 +167,28 @@ pub fn parse(input: &[u8], as_32bit: bool) -> Result, String> { )); } let tag = if as_32bit { - u64::from_le_bytes(input[pos..pos + 8].try_into().unwrap()) as u32 + u64::from_le_bytes( + input[pos..pos + 8] + .try_into() + .expect("8-byte tag slice in bounds (validated above)"), + ) as u32 } else { - u32::from_le_bytes(input[pos..pos + 4].try_into().unwrap()) + u32::from_le_bytes( + input[pos..pos + 4] + .try_into() + .expect("4-byte tag slice in bounds (validated above)"), + ) }; pos += tag_size; - let flags = u16::from_le_bytes(input[pos..pos + 2].try_into().unwrap()); + let flags = u16::from_le_bytes( + input[pos..pos + 2].try_into().expect("2-byte flags slice in bounds (validated above)"), + ); pos += 2; - let len = u32::from_le_bytes(input[pos..pos + 4].try_into().unwrap()) as usize; + let len = u32::from_le_bytes( + input[pos..pos + 4] + .try_into() + .expect("4-byte length slice in bounds (validated above)"), + ) as usize; pos += 4; if pos + len > input.len() { return Err(format!( diff --git a/src/metrics.rs b/src/metrics.rs index 05895388..24f6c6c6 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -54,11 +54,11 @@ impl MetricsRegistry { Self { counters: Mutex::new(HashMap::new()), gauges: Mutex::new(HashMap::new()) } } pub fn counter(&self, name: &str) -> Arc { - let mut m = self.counters.lock().unwrap(); + let mut m = self.counters.lock().expect("MetricsRegistry counters mutex poisoned"); m.entry(name.to_string()).or_insert_with(|| Arc::new(Counter::new())).clone() } pub fn gauge(&self, name: &str) -> Arc { - let mut m = self.gauges.lock().unwrap(); + let mut m = self.gauges.lock().expect("MetricsRegistry gauges mutex poisoned"); m.entry(name.to_string()).or_insert_with(|| Arc::new(Gauge::new())).clone() } } diff --git a/src/monitoring.rs b/src/monitoring.rs index dfef3d7b..9d37e32b 100644 --- a/src/monitoring.rs +++ b/src/monitoring.rs @@ -228,7 +228,7 @@ impl MonitoringReport { } fn now_secs() -> u64 { - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + SystemTime::now().duration_since(UNIX_EPOCH).expect("system clock before Unix epoch").as_secs() } #[cfg(test)] diff --git a/src/object_pool.rs b/src/object_pool.rs index 37d0e122..c9b27ad8 100644 --- a/src/object_pool.rs +++ b/src/object_pool.rs @@ -10,7 +10,7 @@ impl ObjectPool { Self { items: Arc::new(Mutex::new(VecDeque::new())), capacity } } pub fn put(&self, item: T) -> bool { - let mut g = self.items.lock().unwrap(); + let mut g = self.items.lock().expect("ObjectPool items mutex poisoned"); if g.len() >= self.capacity { false } else { @@ -19,10 +19,10 @@ impl ObjectPool { } } pub fn take(&self) -> Option { - self.items.lock().unwrap().pop_front() + self.items.lock().expect("ObjectPool items mutex poisoned").pop_front() } pub fn available(&self) -> usize { - self.items.lock().unwrap().len() + self.items.lock().expect("ObjectPool items mutex poisoned").len() } pub fn capacity(&self) -> usize { self.capacity diff --git a/src/radix_trie.rs b/src/radix_trie.rs index d306faa2..f0a05ca3 100644 --- a/src/radix_trie.rs +++ b/src/radix_trie.rs @@ -66,7 +66,12 @@ impl RadixTrie { } if !r_tail.is_empty() { split_node.children.push((r_tail, Box::new(Node::new()))); - split_node.children.last_mut().unwrap().1.is_word = true; + split_node + .children + .last_mut() + .expect("split node just pushed a child") + .1 + .is_word = true; } else { split_node.is_word = true; } diff --git a/src/ring_buffer.rs b/src/ring_buffer.rs index 86c6a98b..4f9d1ffc 100644 --- a/src/ring_buffer.rs +++ b/src/ring_buffer.rs @@ -38,7 +38,7 @@ impl RingBuffer { let mut v = Vec::with_capacity(self.size); for i in 0..self.size { let idx = (self.head + i) % self.data.len(); - v.push(self.data[idx].as_ref().unwrap()); + v.push(self.data[idx].as_ref().expect("slot occupied within size bounds")); } v } diff --git a/src/runtime.rs b/src/runtime.rs index c3dddbc7..2f80e9fc 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -87,7 +87,10 @@ impl From for ProcState { } #[derive(Debug, Clone)] -#[allow(dead_code)] // some fields are read only by the lib target, not the bin +// The bin crate only writes most of these fields; they are consumed by the +// tray dashboard consumers (IPC payloads / JSON snapshots) outside this +// crate's compile unit, which the dead-code pass cannot see. +#[allow(dead_code)] pub struct ProcessInfo { pub pid: u32, pub name: String, @@ -223,7 +226,6 @@ fn count_open_fds(pid: u32) -> Option { None } -#[allow(unreachable_code)] // final `None` only reachable on non-linux/macos targets fn count_threads(pid: u32) -> Option { #[cfg(target_os = "linux")] if let Ok(entries) = std::fs::read_dir(format!("/proc/{pid}/task")) { diff --git a/src/s_expression.rs b/src/s_expression.rs index 69abb6a2..91fd6eeb 100644 --- a/src/s_expression.rs +++ b/src/s_expression.rs @@ -38,7 +38,7 @@ pub fn parse(input: &str) -> Result { if v.len() > 1 { return Err("multiple top-level expressions".into()); } - Ok(v.into_iter().next().unwrap()) + Ok(v.into_iter().next().expect("exactly one top-level expression (length checked above)")) } pub fn to_string(s: &Sexp) -> String { diff --git a/src/skiplist.rs b/src/skiplist.rs index 221faebd..3f4f8ea1 100644 --- a/src/skiplist.rs +++ b/src/skiplist.rs @@ -59,7 +59,7 @@ impl SkipList { loop { let next = cur.borrow().forward[i].clone(); if let Some(n) = next { - if n.borrow().key.as_ref().unwrap() < &key { + if n.borrow().key.as_ref().expect("skip node key is always set") < &key { cur = n; continue; } @@ -85,7 +85,7 @@ impl SkipList { loop { let next = cur.borrow().forward[i].clone(); if let Some(n) = next { - if n.borrow().key.as_ref().unwrap() < key { + if n.borrow().key.as_ref().expect("skip node key is always set") < key { cur = n; continue; } @@ -95,7 +95,7 @@ impl SkipList { } let next = cur.borrow().forward[0].clone(); if let Some(n) = next { - if n.borrow().key.as_ref().unwrap() == key { + if n.borrow().key.as_ref().expect("skip node key is always set") == key { return n.borrow().value.clone(); } } diff --git a/src/ssh_known_hosts.rs b/src/ssh_known_hosts.rs index 02283c2f..fbb1464a 100644 --- a/src/ssh_known_hosts.rs +++ b/src/ssh_known_hosts.rs @@ -212,7 +212,7 @@ fn parse_bracket_class(p: &[u8], start: usize) -> Option<(Vec, bool, usize)> while i < p.len() && p[i] != b']' { let c = p[i]; if c == b'-' && last.is_some() && i + 1 < p.len() && p[i + 1] != b']' { - let lo = last.unwrap(); + let lo = last.expect("last is Some when `last.is_some()` was checked above"); let hi_b = p[i + 1]; for b in lo..=hi_b { members.push(b); diff --git a/src/stats.rs b/src/stats.rs index 1e9cd971..8d1a273a 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -18,7 +18,7 @@ pub fn median(mut values: Vec) -> f64 { if values.is_empty() { return 0.0; } - values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + values.sort_by(|a, b| a.partial_cmp(b).expect("f64 ordering is total for non-NaN inputs")); let n = values.len(); if n % 2 == 1 { values[n / 2] diff --git a/src/stream.rs b/src/stream.rs index dabb0f2d..d1bf9cf4 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -8,7 +8,7 @@ impl Stream { Self { chunks: vec![Vec::new()], pos: 0, off: 0 } } pub fn write(&mut self, item: T) { - self.chunks.last_mut().unwrap().push(item); + self.chunks.last_mut().expect("Stream always has at least one chunk").push(item); } pub fn next(&mut self) -> Option { loop { diff --git a/src/theme.rs b/src/theme.rs index 1e4a06cf..40c7454b 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -18,6 +18,10 @@ pub struct Rgb(pub u8, pub u8, pub u8); impl Rgb { /// Build an `Rgb` from a `#rrggbb` or `rrggbb` hex literal at compile time. /// Panics on malformed input — only call with string literals. + // The panic is a documented contract for malformed color literals (the + // caller passes string literals, so the branch is unreachable in practice); + // deliberately kept so misconfiguration fails loudly at startup. + #[allow(clippy::panic)] pub const fn from_hex(hex: &str) -> Self { let bytes = hex.as_bytes(); // accept '#rrggbb' (7) or 'rrggbb' (6)