diff --git a/.capsec-baseline.json b/.capsec-baseline.json new file mode 100644 index 00000000..3941ecbd --- /dev/null +++ b/.capsec-baseline.json @@ -0,0 +1,514 @@ +[ + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/witness/server.rs", + "function": "run_server", + "call_text": "tokio::net::TcpListener::bind", + "category": "NET" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/config.rs", + "function": "from_env", + "call_text": "std::env::var", + "category": "ENV" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/config.rs", + "function": "from_env", + "call_text": "std::env::var", + "category": "ENV" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/config.rs", + "function": "from_env", + "call_text": "std::env::var", + "category": "ENV" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/config.rs", + "function": "from_env", + "call_text": "std::env::var", + "category": "ENV" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/config.rs", + "function": "from_env", + "call_text": "std::env::var", + "category": "ENV" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/config.rs", + "function": "from_env", + "call_text": "std::env::var", + "category": "ENV" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/config.rs", + "function": "from_env", + "call_text": "std::env::var", + "category": "ENV" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/config.rs", + "function": "from_env", + "call_text": "std::env::var", + "category": "ENV" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/config.rs", + "function": "from_env", + "call_text": "std::env::var", + "category": "ENV" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/config.rs", + "function": "from_env", + "call_text": "std::env::var", + "category": "ENV" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/agent/handle.rs", + "function": "shutdown", + "call_text": "std::fs::remove_file", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/agent/handle.rs", + "function": "shutdown", + "call_text": "std::fs::remove_file", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/agent/handle.rs", + "function": "test_agent_handle_shutdown", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/storage/windows_credential.rs", + "function": "new", + "call_text": "std::fs::create_dir_all", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/storage/windows_credential.rs", + "function": "load_index", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/storage/windows_credential.rs", + "function": "save_index", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/storage/encrypted_file.rs", + "function": "read_data", + "call_text": "std::fs::File::open", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/testing/builder.rs", + "function": "build", + "call_text": "std::process::Command::new", + "category": "PROC" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/testing/builder.rs", + "function": "build", + "call_text": "output", + "category": "PROC" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/api/runtime.rs", + "function": "start_agent_listener_with_handle", + "call_text": "std::fs::create_dir_all", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/api/runtime.rs", + "function": "start_agent_listener_with_handle", + "call_text": "std::fs::remove_file", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/trust/roots_file.rs", + "function": "load", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/trust/roots_file.rs", + "function": "create_temp_roots_file", + "call_text": "std::fs::File::create", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/trust/pinned.rs", + "function": "read_all", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/trust/pinned.rs", + "function": "write_all", + "call_text": "std::fs::create_dir_all", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/trust/pinned.rs", + "function": "write_all", + "call_text": "std::fs::File::create", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/trust/pinned.rs", + "function": "write_all", + "call_text": "std::fs::rename", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/trust/pinned.rs", + "function": "lock", + "call_text": "std::fs::create_dir_all", + "category": "FS" + }, + { + "crate_name": "auths-core", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-core/src/trust/pinned.rs", + "function": "test_concurrent_access_no_corruption", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "install_cache_hooks", + "call_text": "std::fs::create_dir_all", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "install_hook", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "install_hook", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "install_hook", + "call_text": "std::fs::metadata", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "find_git_dir", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "uninstall_cache_hooks", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "uninstall_cache_hooks", + "call_text": "std::fs::remove_file", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "uninstall_cache_hooks", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "install_linearity_hook", + "call_text": "std::fs::create_dir_all", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "install_linearity_hook", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "install_linearity_hook", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "install_linearity_hook", + "call_text": "std::fs::metadata", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "uninstall_linearity_hook", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "uninstall_linearity_hook", + "call_text": "std::fs::remove_file", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "uninstall_linearity_hook", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_install_new_hooks", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_install_appends_to_existing", + "call_text": "std::fs::create_dir_all", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_install_appends_to_existing", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_install_appends_to_existing", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_install_idempotent", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_install_linearity_hook_new", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_install_linearity_hook_idempotent", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_install_linearity_hook_appends_to_existing", + "call_text": "std::fs::create_dir_all", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_install_linearity_hook_appends_to_existing", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_install_linearity_hook_appends_to_existing", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_uninstall_linearity_hook_preserves_other_content", + "call_text": "std::fs::create_dir_all", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_uninstall_linearity_hook_preserves_other_content", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/storage/registry/hooks.rs", + "function": "test_uninstall_linearity_hook_preserves_other_content", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/agent_identity.rs", + "function": "ensure_git_repo", + "call_text": "std::fs::create_dir_all", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/agent_identity.rs", + "function": "write_agent_toml", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/freeze.rs", + "function": "load_active_freeze", + "call_text": "std::fs::read_to_string", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/freeze.rs", + "function": "load_active_freeze", + "call_text": "std::fs::remove_file", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/freeze.rs", + "function": "store_freeze", + "call_text": "std::fs::write", + "category": "FS" + }, + { + "crate_name": "auths-id", + "crate_version": "0.0.1-rc.9", + "file": "crates/auths-id/src/freeze.rs", + "function": "remove_freeze", + "call_text": "std::fs::remove_file", + "category": "FS" + } +] diff --git a/.capsec.toml b/.capsec.toml new file mode 100644 index 00000000..8f6950d5 --- /dev/null +++ b/.capsec.toml @@ -0,0 +1,14 @@ +# capsec audit configuration for auths workspace +# +# Two-tier enforcement: +# - Clean crates (crypto, verifier, policy, keri): --fail-on low (zero findings expected) +# - Dirty crates (core, id): --diff --fail-on high (baseline + regression detection) + +[analysis] +exclude = ["tests/**", "benches/**"] + +# auths-verifier's WASM extern block is expected — it's a minimal FFI binding +# for console.log in wasm32 targets, not a security concern. +[[allow]] +crate = "auths-verifier" +function = "extern \"C\"" diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 34285902..e15f2dae 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -12,4 +12,9 @@ ignore = [ # Only affects the optional witness-server feature, not core signing or # verification paths. "RUSTSEC-2025-0134", + + # rustls-webpki 0.101.7 CRL matching logic error (via aws-smithy-http-client + # -> hyper-rustls 0.24 -> rustls 0.21). Pinned by AWS SDK's legacy TLS stack. + # No update available until AWS SDK drops rustls 0.21 support. + "RUSTSEC-2026-0049", ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3be385c7..ce5612c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,6 +138,34 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} + capsec-audit: + name: Capability Audit + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - name: Audit clean crates (zero I/O expected) + uses: bordumb/capsec-github-action@v1 + with: + only: auths-crypto,auths-verifier,auths-policy,auths-keri + fail-on: low + upload-sarif: false + comment-on-pr: false + - name: Audit dirty crates (no new high-risk I/O) + uses: bordumb/capsec-github-action@v1 + with: + only: auths-core,auths-id + fail-on: high + upload-sarif: true + sarif-category: capsec-audit-dirty + comment-on-pr: true + msrv: name: MSRV check (1.93) runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bb1cc64..4e5ae314 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,6 +56,13 @@ repos: files: (Cargo\.(toml|lock)|deny\.toml)$ pass_filenames: false + - id: capsec-audit + name: capsec audit (I/O boundaries) + entry: bash -c 'command -v cargo-capsec >/dev/null 2>&1 || { echo "Skipping capsec audit — not installed."; exit 0; }; cargo capsec audit --only auths-crypto,auths-verifier,auths-policy,auths-keri --fail-on low --quiet && cargo capsec audit --only auths-core,auths-id --diff --fail-on high --quiet' + language: system + files: \.(rs|toml)$|Cargo\.lock$ + pass_filenames: false + # ── Slow gates (push only) ────────────────────────────────────────── # These run on `git push`. They require linking binaries, compiling # alternative targets, or cross-compilation. diff --git a/Cargo.lock b/Cargo.lock index 59a98e85..ac611358 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,9 +878,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -888,9 +888,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -1800,7 +1800,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -5394,7 +5394,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -5444,7 +5444,7 @@ dependencies = [ "rustls 0.23.37", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.10", "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", @@ -5469,9 +5469,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -7280,7 +7280,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/docs/architecture/ADRs/001-capsec-capabilities-on-adapters.md b/docs/architecture/ADRs/001-capsec-capabilities-on-adapters.md new file mode 100644 index 00000000..f639cf25 --- /dev/null +++ b/docs/architecture/ADRs/001-capsec-capabilities-on-adapters.md @@ -0,0 +1,119 @@ +# ADR-001: capsec Capabilities on Adapters, Not Port Traits + +**Status:** Accepted +**Date:** 2026-03-21 +**Epic:** fn-82 (capsec Type System Adoption) + +## Context + +We are adopting [capsec](https://github.com/bordumb/capsec) to enforce I/O boundaries at compile time. capsec provides zero-sized capability tokens (`Cap

`, `SendCap

`) and a `Has

` trait bound that functions use to declare what I/O they require. + +The auths workspace uses a ports-and-adapters architecture. Port traits (e.g., `BlobReader`, `RegistryClient`, `KeyStorage`) are defined in domain crates (`auths-core`, `auths-id`) and stored as `Arc` in `AuthsContext`. Adapter implementations live in infrastructure crates (`auths-infra-git`, `auths-infra-http`) and the CLI (`auths-cli/adapters/`). + +The question: where do capsec capability bounds go? + +## Decision + +**Capability tokens are held by adapter structs, not declared on port traits.** + +```rust +// Port trait — UNCHANGED, no capsec dependency +pub trait BlobReader: Send + Sync { + fn read_blob(&self, key: &str) -> Result, StorageError>; +} + +// Adapter — holds capability token internally +pub struct GitBlobReader { + repo_path: PathBuf, + fs_cap: SendCap, +} + +impl GitBlobReader { + pub fn new(repo_path: PathBuf, fs_cap: SendCap) -> Self { + Self { repo_path, fs_cap } + } +} + +impl BlobReader for GitBlobReader { + fn read_blob(&self, key: &str) -> Result, StorageError> { + capsec::fs::read(self.repo_path.join(key), &self.fs_cap) + .map_err(|e| StorageError::ReadFailed(e.to_string())) + } +} +``` + +The composition root in `auths-cli` creates `CapRoot`, grants capabilities, and passes `SendCap

` tokens to adapter constructors: + +```rust +let root = capsec::root(); +let fs_cap = root.grant::().make_send(); +let blob_reader: Arc = + Arc::new(GitBlobReader::new(repo_path, fs_cap)); +``` + +## Alternatives Considered + +### Alternative A: `Has

` bounds on port trait definitions + +```rust +pub trait BlobReader: Send + Sync + Has { + fn read_blob(&self, key: &str) -> Result, StorageError>; +} +``` + +**Rejected** because: +- Adding generic `Has

` bounds to traits breaks object safety. `Arc` would no longer compile because `Has

` has a generic parameter. +- The entire `AuthsContext` is built on `Arc` dispatch. This would require a fundamental redesign of the dependency injection container. +- Domain crates (`auths-core`, `auths-id`) would need a capsec dependency, leaking an infrastructure concern into domain logic. + +### Alternative B: `Has

` bounds on individual trait methods + +```rust +pub trait BlobReader: Send + Sync { + fn read_blob(&self, key: &str, cap: &impl Has) -> Result, StorageError>; +} +``` + +**Rejected** because: +- Methods with `impl Trait` parameters are not object-safe. Same `dyn Trait` problem as Alternative A. +- Every caller — including domain logic that should be capsec-unaware — would need to pass capability tokens through. +- Fakes in tests would need dummy capability tokens even though they do no I/O. + +### Alternative C: Separate capsec-aware wrapper traits + +```rust +pub trait CapBlobReader: Send + Sync { + fn read_blob(&self, key: &str, cap: &impl Has) -> Result, StorageError>; +} +``` + +**Rejected** because: +- Duplicates the entire port trait surface area. +- Two parallel hierarchies to maintain. +- Over-engineered for the actual problem. + +## Consequences + +**Positive:** +- Port traits remain object-safe and capsec-free. Domain crates have zero capsec dependency. +- Follows the established clock injection precedent (fn-64): capabilities are created at the CLI boundary and passed down, just like `DateTime` is created via `Utc::now()` at the CLI boundary and injected into domain functions. +- Adapters are the natural place for I/O tokens — they are the I/O boundary by definition. +- Fakes and test doubles need no capsec awareness since port traits are unchanged. Only integration tests that construct real adapters need `capsec::test_root()`. +- Published library crates (`auths-core`, `auths-id`, `auths-verifier`) don't inherit a pre-1.0 dependency. + +**Negative:** +- The compiler cannot prevent an adapter from doing I/O without its capability token — it can still call `std::fs::read()` directly. This is mitigated by `cargo capsec audit` which detects direct `std` calls. +- If an adapter is constructed without the right capability (e.g., someone forgets to pass `SendCap`), the error is at the adapter constructor call site, not at the I/O call site. This is acceptable — the constructor is the API contract. +- Capability tokens are not visible in port trait signatures, so a reader of the trait alone cannot see what I/O the adapter will do. The audit tool's output serves as the capability manifest. + +## Precedent + +This decision mirrors the clock injection pattern established in ADR fn-64: + +| Concern | Clock (fn-64) | Capabilities (fn-82) | +|---------|--------------|---------------------| +| What's banned in domain crates | `Utc::now()` | `std::fs`, `std::net`, `std::process` | +| Created at | CLI boundary (`Utc::now()`) | CLI boundary (`capsec::root()`) | +| Passed via | Function parameter (`now: DateTime`) | Adapter constructor (`SendCap

`) | +| Domain crates see | `DateTime` (a value) | Nothing (capabilities are adapter-internal) | +| Enforcement | clippy.toml bans `Utc::now` | clippy.toml bans `std::fs` + capsec audit |