mati 0.1.4

An enforcement layer for codebase knowledge: confirmed gotchas gate what AI agents read and edit at the hook level. Not a passive memory store.
Documentation
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1
  # Fail rustdoc on broken intra-doc links, private items linked from
  # public APIs, etc. Surfaces breakage that `cargo check` doesn't catch.
  RUSTDOCFLAGS: -D warnings

jobs:
  check:
    name: Check & Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - uses: Swatinem/rust-cache@v2

      - name: cargo fmt
        run: cargo fmt --all -- --check

      - name: cargo clippy
        run: cargo clippy --locked --all-targets -- -D warnings

      - name: cargo check
        run: cargo check --locked --all-targets

      # Verify the binary builds without default features so the `semantic`
      # feature gate (candle / usearch) cannot rot silently.
      - name: cargo check --no-default-features
        run: cargo check --locked --no-default-features --all-targets

      # rustdoc with -D warnings (set via RUSTDOCFLAGS at the workflow level)
      # catches broken intra-doc links and other doc-only regressions.
      - name: cargo doc
        run: cargo doc --locked --no-deps --all-features

  # MSRV CI gate intentionally NOT enforced.
  #
  # ADR-008 documents MSRV at 1.82 as project intent. However, transitive
  # deps in the Rust ecosystem (notably clap_lex 1.1.0+) ship Cargo.toml
  # manifests using syntax newer than the 1.82 Cargo can parse, so a
  # `runs-on: rust-toolchain@1.82.0` check fails on manifest parsing
  # before reaching mati's own source. The MSRV declaration in Cargo.toml
  # remains advisory documentation; enforcement would require either
  # (a) pinning specific transitive versions (brittle) or (b) bumping
  # MSRV and updating ADR-008.

  test:
    name: Test (${{ matrix.os }})
    # Run the default-included test suite on both Linux and macOS.
    # macOS coverage exists because mati ships as a Claude Code plugin and
    # most users run it on macOS — APFS fsync semantics, BSD socket nuances,
    # and the `logd` watchdog have caused real failures invisible to a
    # Linux-only matrix.
    #
    # SKIPPED ON PULL REQUESTS: this job takes >15min on cold cache and
    # blocks PR feedback. It still runs on every push to main so coverage
    # is preserved after merge. To re-enable for PRs, remove the `if:` line.
    if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
    strategy:
      fail-fast: false
      matrix:
        # macOS is dispatch-only: 10x runner cost vs Linux.
        os: ${{ github.event_name == 'workflow_dispatch' && fromJSON('["ubuntu-latest","macos-latest"]') || fromJSON('["ubuntu-latest"]') }}
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - uses: Swatinem/rust-cache@v2

      # Use cargo-nextest for the default test pass. Nextest runs each
      # test in its own subprocess (vs cargo test's shared-binary model),
      # which prevents tokio runtime / SurrealKV / OnceLock state from
      # accumulating across hundreds of tests in one process. The matching
      # local profile is the `default` profile in `.config/nextest.toml`.
      - uses: taiki-e/install-action@nextest

      - name: cargo nextest run
        run: cargo nextest run --locked --all-targets --profile ci

      # Nextest does not run doc tests (rustdoc API limitations). The
      # `--doc` pass remains on vanilla `cargo test`. This stays
      # single-binary but doc tests are short and CPU-light, so it's fine.
      - name: cargo test --doc
        run: cargo test --locked --doc

      # End-to-end policy engine smoke. Unit tests cover the logic; this drives
      # the real binary, a real daemon, and the hook subprocess protocol, which
      # is the only way to catch mint/check scope mismatches and config gating
      # bugs that a same-actor unit test cannot express.
      #
      # Exit 77 means the environment cannot bind a Unix domain socket, so the
      # daemon never starts and every hook correctly fails open. That is a skip,
      # not a failure.
      - name: policy engine smoke
        run: |
          set +e
          ./scripts/smoke-policy.sh
          rc=$?
          set -e
          if [ "$rc" -eq 77 ]; then
            echo "::notice::policy engine smoke skipped (no Unix domain socket available)"
            exit 0
          fi
          exit "$rc"

  ignored-tests:
    # The `#[ignore]`d integration tests cover subprocess lifecycle,
    # crash recovery, concurrent socket dispatch, and the panic-hook
    # pre-opened-fd path. They're slow and depend on a writable `~/`,
    # so they don't run by default — but they do gate merges.
    #
    # DISPATCH-ONLY: these are slow subprocess tests. Run them by hand
    # before a release, not on every push.
    name: Ignored integration tests (${{ matrix.os }})
    if: github.event_name == 'workflow_dispatch'
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      # Full history, not the default shallow (fetch-depth: 1) clone:
      # smoke_git_history_accessible asserts the mati repo has >=5 commits,
      # which a 1-commit shallow checkout fails.
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: dtolnay/rust-toolchain@stable

      - uses: Swatinem/rust-cache@v2

      - uses: taiki-e/install-action@nextest

      - name: cargo nextest run -- --ignored
        # Ignored subprocess tests race each other on `~/.mati/<slug>/`
        # lifecycle logs and per-project sockets, so they need serial
        # execution. `--test-threads=1` and nextest's per-subprocess
        # isolation together give the strongest guarantee.
        # `stress_700000_edges_linux_kernel_scale` runs here too and is
        # fine at threads=1.
        #
        # QUARANTINED via `-E` — two daemon-startup races that fail
        # deterministically on the CI runners (all retries) but pass locally,
        # so they cannot be fixed blind. Tracked as WI-21; re-include each as
        # its race is fixed:
        #   - serve_exits_cleanly_on_sigterm_after_client_disconnect: drops
        #     stdin before the MCP `initialize` completes -> `serve_failed`
        #     instead of the clean idle-wait -> SIGTERM path.
        #   - metrics_snapshot_reflects_dispatch_traffic: queries the metrics
        #     snapshot before it is initialized -> `version: None`.
        # EXCLUDED — `real_world_verification` clones external repos over the
        # network. That is a dependency on GitHub being up and on CI minutes
        # spent fetching, neither of which tells us anything about mati. Run
        # them locally: `cargo nextest run --run-ignored only -E
        # 'binary(real_world_verification)'`.
        #
        # `not kind(bench)` is restated so the filterset is self-contained
        # regardless of how `-E` composes with the profile default-filter.
        run: cargo nextest run --locked --all-targets --profile ci --run-ignored only --test-threads 1 -E 'not kind(bench) and not binary(real_world_verification) and not test(serve_exits_cleanly_on_sigterm_after_client_disconnect) and not test(metrics_snapshot_reflects_dispatch_traffic)'

      # Run the highest-value real-code resolver checks without network access.
      # These sources are vendored under tests/fixtures, and each test falls
      # back to cloning only when its fixture is absent for local use.
      - name: real-world resolvers (vendored fixtures)
        run: cargo nextest run --locked --profile ci --run-ignored only --test real_world_verification --test-threads 1 -E 'test(real_world_ripgrep) or test(real_world_sinatra_resolution) or test(real_world_hcl_resolution) or test(real_world_jsoup_resolution) or test(real_world_httpx)'

  supply-chain:
    name: Supply chain (cargo-deny)
    runs-on: ubuntu-latest
    # Broader supply-chain gate than `cargo audit` alone. Runs three of
    # cargo-deny's four checks against `deny.toml`:
    #
    #   bans       — duplicate versions + wildcard version specifiers
    #   advisories — the RUSTSEC vulnerability stream, plus optional
    #                yanked-crate detection
    #   sources    — only crates.io, no git deps, no unknown registries
    #
    # The fourth check (licenses) is INTENTIONALLY SKIPPED. Customer
    # license-key validation belongs in mati-cloud per CLAUDE.md "What
    # this repo must NEVER include". Dependency license scanning for the
    # commercial consumer is mati-cloud's responsibility on its final
    # binary, not ours upstream.
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - name: Install cargo-deny
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-deny

      - name: cargo deny check (bans + advisories + sources)
        run: cargo deny check bans advisories sources

      # Zero-network attestation: the DEFAULT build — what `cargo install mati`
      # ships, and what runs the enforcement hot path — must link NO network /
      # HTTP-client crate. This makes CLAUDE.md's invariant ("DENY/ALLOW/receipt
      # is local, zero network, always ... never phones home") machine-enforced
      # and a permanent regression guard. The opt-in `semantic` feature downloads
      # an embedding model via hf-hub and is the one documented exception, so it
      # is intentionally excluded here (this check uses --no-default-features).
      - name: Zero-network attestation (default build links no HTTP client)
        run: |
          set -euo pipefail
          net=$(cargo tree --no-default-features --edges normal --prefix none --format '{p}' \
                | awk '{print $1}' | sort -u \
                | grep -iE '^(reqwest|hyper|hyper-util|hyper-rustls|hyper-tls|h2|ureq|isahc|surf|attohttpc|curl|curl-sys)$' \
                || true)
          if [ -n "$net" ]; then
            echo "::error::network-capable crate(s) in the DEFAULT (enforcement) build — violates the zero-network invariant (CLAUDE.md):"
            echo "$net"
            exit 1
          fi
          echo "OK: default build links no network/HTTP-client crate"

  no-network:
    name: No-network runtime attestation
    runs-on: ubuntu-latest
    # Behavioral companion to the build-time "Zero-network attestation" above.
    # The dep-ban proves no HTTP-CLIENT CRATE is linked; it structurally CANNOT
    # prove the absence of a raw socket call — std::net, tokio/net (which
    # `tokio = ["full"]` enables in the default build), or a shelled-out fetch.
    # This job proves the property at the SYSCALL boundary instead, which is
    # dependency-agnostic:
    #
    #   (1) syscall audit  — run the enforcement path under strace and FAIL on
    #       any connect()/sendto()/sendmsg() to an AF_INET/AF_INET6 address. This
    #       is the airtight "never phones home / zero telemetry" proof: it catches
    #       ANY outbound IP connection regardless of how it is made.
    #   (2) offline liveness — run the same path with networking removed (network
    #       namespace) and assert it still works, proving the hot path has no
    #       hidden network dependency. (Local Unix-socket IPC is unaffected — it
    #       is filesystem-scoped, not network-namespace-scoped.)
    #
    # MATI_DISABLE_AUTO_SPAWN=1 keeps the background daemon from forking, so the
    # traced process tree stays bounded and uses direct store access.
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2

      - name: Install strace
        run: sudo apt-get update -qq && sudo apt-get install -y -qq strace

      - name: Build default (enforcement) binary
        run: cargo build --locked --bin mati

      - name: No-network runtime attestation
        run: |
          set -euo pipefail
          BIN="$PWD/target/debug/mati"
          export MATI_DISABLE_AUTO_SPAWN=1

          work="$(mktemp -d)"
          export HOME="$work/home"; mkdir -p "$HOME"
          repo="$work/repo"; mkdir -p "$repo"
          git -C "$repo" init -q
          git -C "$repo" config user.email ci@example.com
          git -C "$repo" config user.name  ci
          printf 'fn main() { println!("hi"); }\n' > "$repo/main.rs"
          git -C "$repo" add -A
          git -C "$repo" -c commit.gpgsign=false commit -q -m init

          # JSON payload for the hook (file avoids nested-quoting in bash -c).
          payload="$work/payload.json"
          printf '%s' '{"session_id":"ci","tool_input":{"file_path":"main.rs"}}' > "$payload"

          # Enforcement-relevant surface. Exit codes are tolerated (doctor exits
          # non-zero as a CI gate; hook-decide may deny) — the audit inspects
          # SYSCALLS, not status. $0=binary, $1=repo, $2=payload.
          seq='
            "$0" init --path "$1" --no-hooks      >/dev/null 2>&1 || true
            "$0" doctor --json                    >/dev/null 2>&1 || true
            ( cd "$1" && "$0" status )            >/dev/null 2>&1 || true
            "$0" hook-decide claude-pre-read <"$2" >/dev/null 2>&1 || true
          '

          echo "== (1) syscall audit: no outbound IP connection during the enforcement path =="
          trace="$work/net.strace"
          strace -f -e trace=%network -o "$trace" bash -c "$seq" "$BIN" "$repo" "$payload" || true
          # Transparency: report any AF_INET socket() even if never connected.
          if grep -qE 'socket\(AF_INET' "$trace"; then
            echo "note: an AF_INET/AF_INET6 socket() was created (not necessarily egress):"
            grep -nE 'socket\(AF_INET' "$trace" | head
          fi
          egress=$(grep -nE '(connect|sendto|sendmsg)\([^)]*sa_family=AF_INET' "$trace" || true)
          if [ -n "$egress" ]; then
            echo "::error::enforcement path attempted an outbound IP connection — violates zero-network / never-phones-home:"
            echo "$egress"
            exit 1
          fi
          echo "  OK: zero connect()/sendto()/sendmsg() to any AF_INET/AF_INET6 address"

          echo "== (2) offline liveness: enforcement path runs with networking removed =="
          if unshare -rn true 2>/dev/null; then UNS="unshare -rn";
          elif sudo unshare -n true 2>/dev/null; then UNS="sudo -E unshare -n";
          else UNS=""; fi
          if [ -z "$UNS" ]; then
            echo "::warning::no usable network-namespace isolation on this runner; skipping offline-liveness (the syscall audit already proves no egress)"
          else
            echo "  using: $UNS"
            $UNS env HOME="$HOME" MATI_DISABLE_AUTO_SPAWN=1 "$BIN" init --path "$repo" --no-hooks >/dev/null
            echo "  OK: 'mati init' completes with no network namespace"
          fi
          echo "zero-network runtime attestation: PASS"