Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}

fuzz-audit:
name: Fuzz dependency audit
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
steps:
- uses: actions/checkout@v7
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
working-directory: fuzz

fmt:
name: Format
runs-on: ubuntu-latest
Expand All @@ -32,6 +45,7 @@ jobs:
with:
components: rustfmt
- run: cargo fmt --all -- --check
- run: cargo fmt --manifest-path fuzz/Cargo.toml --all -- --check

clippy:
name: Clippy
Expand Down Expand Up @@ -73,6 +87,45 @@ jobs:
docker pull redis:7-alpine
- run: cargo test ${{ matrix.flags }}

feature-contracts:
name: Feature contracts (${{ matrix.features }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
features: [protocol-2026-07-28, 'oauth,discovery']
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: contracts-${{ matrix.features }}
- run: cargo test --locked --no-default-features --features ${{ matrix.features }} --test protocol_contracts --test invariant_properties --test config_preparation

fuzz:
name: Bounded fuzz smoke
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-10-19
- uses: Swatinem/rust-cache@v2
with:
workspaces: fuzz
key: fuzz-nightly-2026-10-19
- uses: taiki-e/install-action@v2
with:
tool: cargo-fuzz@0.13.2
- run: ./scripts/fuzz-smoke.sh
- uses: actions/upload-artifact@v7
if: failure()
with:
name: fuzz-reproducers
path: fuzz/artifacts/
if-no-files-found: ignore

msrv:
name: MSRV (1.90, ${{ matrix.features }})
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ CLAUDE.md
proxy*.toml
!config.example.toml
!examples/docker-compose/proxy.toml
/fuzz/target/
/fuzz/artifacts/
/fuzz/corpus/
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ tests/
chaos.rs # Fault injection and resilience recovery regressions
cache_backends.rs # Real Redis containers and temporary SQLite databases
config_properties.rs # Property tests for config validation
invariant_properties.rs # Config/authorization/transform properties and paused-clock schedules
protocol_contracts.rs # Configured library/CLI legacy baseline and modern feature contracts
examples.rs # Load and validate every example TOML config

examples/
Expand Down Expand Up @@ -152,6 +154,20 @@ paused-clock competing recovery, late completion and removal/replacement tests.
Manual detector accounting is separate and extra releases cannot consume owned
reservations. See outlier rustdoc for quota shrink and compatibility semantics.

### Invariant and protocol regressions

`tests/invariant_properties.rs` and `tests/protocol_contracts.rs` complement the
configured runtime regressions. Preserve the legacy `2025-11-25` negotiation
baseline and feature-gated modern per-request contract; never treat a successful
HTTP response alone as permission to dispatch. Reduced protocol-only and
OAuth/discovery builds must keep feature-negative configuration failures before
effects. Pure parser/transform assertions in `tests/support/invariants.rs` are
shared with the isolated, locked `fuzz/` workspace. Run `scripts/fuzz-smoke.sh`
with the pinned tools for changes to these assertions/targets/runner. Keep finite
input/run/time/RSS limits and temporary mutation corpora; promote minimized
failures to tracked seeds and focused regressions. See CONTRIBUTING.md for the
coverage matrix and reproduction commands.

### Adding a new middleware layer

Every middleware follows the same tower Service pattern. Use `inject.rs` as a template:
Expand Down
66 changes: 66 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,72 @@ separate test matrix and are not included in this coverage report. Line coverage
is a guide to missing tests, not proof of correctness. No percentage gate is set
until there is a representative baseline and a reason for the chosen threshold.

## Invariants, protocol versions and bounded fuzzing

The following checks complement the default/all/no-default-feature suites. They
exercise these contracts, rather than claiming exhaustive protocol conformance:

| Contract | Regression coverage |
| --- | --- |
| Legacy initialization selects the proxy's supported `2025-11-25` baseline, including when the client requests an older, modern or unknown revision | `protocol_contracts`: configured library and real CLI; initialized notification, exact listing/call/denial, injection and session cleanup |
| `2026-07-28` requests use per-request metadata plus matching HTTP method/name headers, without initialization or a frontend session | `protocol_contracts`: both entry points; explicit unsupported-version error without the feature; malformed metadata/header and invalid credential denials make zero tool calls |
| Feature-disabled configuration fails before backend, store or IdP effects | `config_preparation`: prepare/library/CLI check/startup; OAuth, discovery, telemetry, metrics, WebSocket, Redis, SQLite and CLI YAML rejection |
| Preparation is idempotent; equivalent representable JSON/TOML/YAML revisions agree; invalid revisions do not partially mutate configuration | `invariant_properties`: bounded generated configurations |
| Role order/duplicates cannot widen grants; deny and static filter win; management remains separate | `invariant_properties`: generated JWT roles through a configured proxy, direct/search calls, aliased names, exact transformed arguments and backend call counts |
| Alias/parameter/injection transformations preserve request IDs, continuation/task/meta fields and extensions | `invariant_properties` and `request_transform` fuzz target share `support/invariants.rs` assertions against the actual services |
| Cancelled followers free capacity; completion/cancellation/expiry releases the generation without replay | `invariant_properties`: generated controlled future schedules with a paused clock; existing configured coalescing regressions remain required |
| Bounded parser inputs do not panic, and accepted configurations round-trip | `invariant_properties` and `config_parse` fuzz target; parsing only, never configuration preparation or runtime startup |

The sessionful proxy advertises `2025-11-25`; recognizing older transport headers
does not promise negotiation of every older protocol revision. The optional
modern protocol is selected by per-request metadata, not by requesting it in
legacy initialization. These tests establish single-process behavior; replica
portability remains separate work.

CI also tests two reduced builds (`--no-default-features --features
protocol-2026-07-28` and `--no-default-features --features oauth,discovery`) against
`protocol_contracts`, `invariant_properties` and `config_preparation`. Run each
feature mode sequentially when sharing a target directory, as described below.

Install the pinned fuzz tools and run the same finite, AddressSanitizer and
coverage-instrumented smoke checks used by CI:

```sh
rustup toolchain install nightly-2026-10-19 --profile minimal
cargo install cargo-fuzz --version 0.13.2 --locked
./scripts/fuzz-smoke.sh
```

The isolated `fuzz/Cargo.toml` workspace has its own committed lockfile and uses
the same approved upstream Git revision as the main package. CI audits both
lockfiles for known security advisories. The runner fetches
that lock with `--locked`, runs cargo-fuzz offline, and checks that the lock did
not change (cargo-fuzz 0.13.2 has no `--locked` flag). It explicitly selects the
installed Rust compiler's host target: prebuilt cargo-fuzz binaries may otherwise
select static musl, which is incompatible with AddressSanitizer. Each target has
a 4 KiB input limit, 256 runs, a 30-second total execution limit, a 5-second per-input
timeout and a 1 GiB RSS limit. The CI job has a separate 10-minute build/run
budget. Property cases and generated structures are also bounded; concurrency
properties do not depend on sleeps, handshake retries or timing luck.

Checked-in `fuzz/seeds/` inputs are also exercised by ordinary tests. The smoke
runner copies them into a temporary corpus, so mutations do not dirty the
checkout. Failures leave reproducers in `fuzz/artifacts/` (uploaded by CI).
To reproduce one using the same host selection:

```sh
fuzz_host=$(rustc +nightly-2026-10-19 -vV | sed -n 's/^host: //p')
cargo +nightly-2026-10-19 fuzz run TARGET PATH_TO_ARTIFACT --dev --target "$fuzz_host"
```

Minimize it with cargo-fuzz's `tmin` (also pass `--target "$fuzz_host"`), then
promote the smallest useful input into `fuzz/seeds/TARGET/` and add a focused
regression for its expected behavior. Fuzz inputs only reach pure parsing and
transformation code; they cannot spawn backends, resolve secrets or contact
stores/identity providers. A bounded
smoke run is an ongoing regression check, not evidence that all inputs or
concurrent schedules have been explored.

## Configured application contracts

Keep fast middleware/unit tests, but use `Proxy::from_config` or the executable
Expand Down
Loading
Loading