diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6e4cd84..6a526fa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,20 @@ version: 2 # Setting target-branch would make this depend on undocumented security-update targeting # behavior for no benefit. updates: + - package-ecosystem: "cargo" + directory: "/reference-implementations/agt-rust" + schedule: + interval: "weekly" + day: "monday" + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + commit-message: + prefix: "deps" + labels: + - "dependencies" + - "rust" + # GitHub Actions run with write access to the repository. Actions are pinned # to commit SHAs in the workflows, and Dependabot understands SHA pins and # updates them along with the version comment. diff --git a/.github/workflows/reference-implementation.yml b/.github/workflows/reference-implementation.yml index 6222485..528281a 100644 --- a/.github/workflows/reference-implementation.yml +++ b/.github/workflows/reference-implementation.yml @@ -83,3 +83,88 @@ jobs: # schemas in this repository rather than a copy of them. - name: Run the tests run: bun test + + agt-rust: + runs-on: ubuntu-latest + env: + ACS_OPA_PATH: ${{ github.workspace }}/reference-implementations/agt/node_modules/agent-control-specification-opa-linux-x64/bin/opa + permissions: + contents: read + defaults: + run: + working-directory: reference-implementations/agt-rust + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install Rust tooling + run: | + rustup toolchain install 1.98.1 --profile minimal --component clippy,rustfmt + rustup default 1.98.1 + + - name: Install Bun for locked AGT dependencies + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: "1.3.11" + + - name: Install TypeScript Guardian dependencies + working-directory: reference-implementations/agt + run: bun install --frozen-lockfile --linker=hoisted + + - name: Check Rust formatting + run: cargo fmt --all -- --check + + - name: Check Rust workspace + run: cargo check --workspace --locked + + - name: Lint Rust workspace + run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings + + - name: Test Rust Guardian with AGT policy + env: + ACS_OPA_TIMEOUT_MS: "30000" + run: cargo test --workspace --all-features --locked + + - name: Compare live Rust and TypeScript Guardians + shell: bash + run: | + set -euo pipefail + + cargo run --locked > "$RUNNER_TEMP/rust-guardian.log" 2>&1 & + rust_pid=$! + + ( + cd ../agt + ACS_GUARDIAN_PORT=8788 bun run guardian + ) > "$RUNNER_TEMP/ts-guardian.log" 2>&1 & + ts_pid=$! + + cleanup() { + kill "$rust_pid" "$ts_pid" 2>/dev/null || true + } + trap cleanup EXIT + + wait_for_guardian() { + local url="$1" + local log="$2" + for _ in {1..60}; do + if curl --silent --show-error --fail \ + --request POST \ + --header "content-type: application/json" \ + --data '{}' \ + "$url" >/dev/null; then + return 0 + fi + sleep 1 + done + echo "Guardian did not become ready at $url" + cat "$log" + return 1 + } + + wait_for_guardian "http://127.0.0.1:8787/acs" "$RUNNER_TEMP/rust-guardian.log" + wait_for_guardian "http://127.0.0.1:8788/acs" "$RUNNER_TEMP/ts-guardian.log" + + bun tests/compare-guardians.mjs diff --git a/reference-implementations/agt-rust/.gitignore b/reference-implementations/agt-rust/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/reference-implementations/agt-rust/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/reference-implementations/agt-rust/Cargo.lock b/reference-implementations/agt-rust/Cargo.lock new file mode 100644 index 0000000..39c6533 --- /dev/null +++ b/reference-implementations/agt-rust/Cargo.lock @@ -0,0 +1,2953 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "acs-agt-guardian" +version = "0.1.0" +dependencies = [ + "agent_control_specification", + "axum", + "chrono", + "jsonschema", + "regex", + "reqwest", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "agent_control_specification" +version = "0.3.1-beta.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0ec5160192f04588a42ed3ba3907965d8e32e9a0ed8e591fd9209ceb44c063" +dependencies = [ + "agent_control_specification_core", + "serde", + "serde_json", +] + +[[package]] +name = "agent_control_specification_core" +version = "0.3.1-beta.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784d93bb6a74f10a913bc3005e6550eba631ed8def2d82864e481be8823c0816" +dependencies = [ + "base64 0.22.1", + "cedar-policy", + "regex", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "ureq", + "url", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "bytes", + "cfg_aliases", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cedar-policy" +version = "4.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4f5e46c425491b7133eecb3030d82d27301b84bfa08533c59a6902cca7eb80" +dependencies = [ + "cedar-policy-core", + "cedar-policy-formatter", + "itertools 0.15.0", + "linked-hash-map", + "linked_hash_set", + "miette", + "ref-cast", + "semver", + "serde", + "serde_json", + "serde_with", + "smol_str", + "thiserror", +] + +[[package]] +name = "cedar-policy-core" +version = "4.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a30d11033d59b262bbb380d8bb5dc5c3dd2dd2281675c558ef4fd8a161ce669" +dependencies = [ + "chrono", + "educe", + "either", + "itertools 0.15.0", + "lalrpop", + "lalrpop-util", + "linked-hash-map", + "linked_hash_set", + "miette", + "nonempty", + "ref-cast", + "regex", + "rustc-literal-escaper", + "serde", + "serde_json", + "serde_with", + "smol_str", + "stacker", + "thiserror", + "unicode-security", +] + +[[package]] +name = "cedar-policy-formatter" +version = "4.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a2c8dc671e62597c85caa11694365b76050abcd9f2177f0520ccac0580f9a41" +dependencies = [ + "cedar-policy-core", + "itertools 0.15.0", + "logos", + "miette", + "pretty", + "regex", + "smol_str", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.6", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "educe" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e451fac8dd8dece16234604bf1efce6e90fddd8ab6ad4d66eec0eca5160959dd" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "fluent-uri" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.9", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonschema" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d46662859bc5f60a145b75f4632fbadc84e829e45df6c5de74cfc8e05acb96b5" +dependencies = [ + "ahash", + "base64 0.22.1", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "idna", + "itoa", + "num-cmp", + "num-traits", + "once_cell", + "percent-encoding", + "referencing", + "regex", + "regex-syntax", + "serde", + "serde_json", + "uuid-simd", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.14.0", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +dependencies = [ + "regex-automata", + "rustversion", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +dependencies = [ + "serde", +] + +[[package]] +name = "linked_hash_set" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata", + "regex-syntax", + "syn 2.0.119", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "miette-derive", + "serde", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" +dependencies = [ + "serde", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.14.2", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width 0.2.2", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "referencing" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9c261f7ce75418b3beadfb3f0eb1299fe8eb9640deba45ffa2cb783098697d" +dependencies = [ + "ahash", + "fluent-uri", + "once_cell", + "parking_lot", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.9", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc-literal-escaper" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe6f213fb658c8fb95baabd5420393438cf5a98d707f5dd701d9197c705f71e" + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap 2.14.2", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.2", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "term" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-security" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e4ddba1535dd35ed8b61c52166b7155d7f4e4b8847cec6f48e71dc66d8b5e50" +dependencies = [ + "unicode-normalization", + "unicode-script", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/reference-implementations/agt-rust/Cargo.toml b/reference-implementations/agt-rust/Cargo.toml new file mode 100644 index 0000000..bbe3413 --- /dev/null +++ b/reference-implementations/agt-rust/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "acs-agt-guardian" +version = "0.1.0" +edition = "2021" +rust-version = "1.89" +publish = false +license = "Apache-2.0" + +[dependencies] +agent_control_specification = "=0.3.1-beta.0" +axum = "0.8" +chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } +jsonschema = { version = "0.33", default-features = false } +regex = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" +sha2 = "0.10" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync"] } +url = "2" + +[dev-dependencies] +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +tempfile = "3" diff --git a/reference-implementations/agt-rust/README.md b/reference-implementations/agt-rust/README.md new file mode 100644 index 0000000..1696ac5 --- /dev/null +++ b/reference-implementations/agt-rust/README.md @@ -0,0 +1,68 @@ +# Rust AGT Guardian (first stage) + +This is a Rust Guardian for [issue #88](https://github.com/GenAI-Security-Project/agent-control-standard/issues/88). It runs beside the [TypeScript AGT reference implementation](../agt/README.md), using its current `mapping.yaml`, AGT manifest and policy bundle, and this repository's ACS v0.1.0 schemas. It is a proof of concept. It does not claim ACS-Core conformance. + +## What runs + +`POST /acs` receives JSON-RPC envelopes. The Guardian validates the envelope and the two supported tool payloads against `specification/v0.1.0/`, assembles an AGT snapshot, calls the pinned `agent_control_specification` Rust SDK (0.3.1-beta.0), and maps its verdict through the shared `mapping.yaml`. A session store keeps source labels and a local hash chain. The server records request and response envelopes in the same JSONL shape as the TypeScript Guardian. + +The TypeScript Claude Code and OpenCode shims can point to this server through `ACS_GUARDIAN_URL`. Those shims are the host side of this stage; this directory does not contain Rust replacements for them. + +## Prerequisites and run + +- Rust 1.89 or newer (the current dependency graph includes Cedar Policy 4.13). +- OPA **0.70.0**, the version used by the pinned AGT policy bundle. Set `ACS_OPA_PATH` to its executable path. Startup checks the reported version and refuses other versions before loading the manifest. CI uses the AGT platform package verified by `../agt/bun.lock`. +- Run from this checkout. The default paths deliberately resolve to the sibling `agt/` tree and the repository schemas. + +```sh +cd reference-implementations/agt-rust +export ACS_OPA_PATH=/absolute/path/to/opa +cargo build --locked +cargo run --locked +``` + +The endpoint is `http://127.0.0.1:8787/acs` by default. To use the existing Claude Code shim, start the Guardian above, then run the hook from `reference-implementations/agt`: + +```sh +echo '{"session_id":"demo","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"echo rm -rf /"}}' | bun run hosts/claude-code/acs-hook.ts +``` + +The shim's `ACS_GUARDIAN_URL` defaults to the same endpoint. The shim does not execute the command. + +The Guardian reads `ACS_GUARDIAN_HOST`, `ACS_GUARDIAN_PORT`, `ACS_MANIFEST_PATH`, `ACS_ON_DECISION_FAILURE`, `ACS_ENVELOPE_LOG`, `ACS_SESSION_CONTEXT_LOG`, and `ACS_OPA_PATH`. It binds to loopback and declares `proceed` on decision delivery failure by default. `ACS_ON_DECISION_FAILURE=deny` changes the handshake's declared posture; other values stop startup. The local logs default to `agt/.acs/envelopes-rust.jsonl` and `agt/.acs/session-context-rust.jsonl`. They can contain raw arguments and should be protected as sensitive local data. They are ignored by Git. + +The AGT SDK reads `ACS_OPA_TIMEOUT_MS`, defaulting to 5000 milliseconds. The handshake advertises this evaluator timeout plus 1000 milliseconds for request handling and decision delivery. The default host deadline is therefore 6000 milliseconds; setting `ACS_OPA_TIMEOUT_MS=30000` advertises 31000 milliseconds. A timeout too large to represent with the delivery margin stops startup. + +## Test + +```sh +cargo fmt --all -- --check +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +ACS_OPA_PATH=/absolute/path/to/opa ACS_OPA_TIMEOUT_MS=30000 cargo test --workspace --all-features +``` + +Six unit tests cover verdict mapping, session state, OPA version validation, and host deadline calculation. The HTTP integration test starts a real Guardian and uses the pinned AGT bundle for handshake, allow, deny, request and result redaction, egress denial, WebFetch allowance, malformed payload, unknown method, parse error, oversized body, and log cases. It also checks the advertised deadline. It requires OPA. The test command gives slow test machines a longer OPA subprocess deadline and correspondingly extends the host deadline. The existing TypeScript tests are a separate baseline and can be run from `../agt` with Bun 1.3.11. + +For a direct wire comparison, start the Rust Guardian on port 8787 and the TypeScript Guardian on port 8788 with the same AGT policy bundle, then run `node tests/compare-guardians.mjs` from this directory. `RUST_GUARDIAN_URL` and `TS_GUARDIAN_URL` can override the endpoints. CI also runs these nine cases: eight complete response comparisons and one handshake comparison excluding the intentionally different `timeout_config.default_ms`. + +This is evidence for those nine cases, not general parity. The comparison does not cover non-object arguments, handshake validation error details, alternative UUID or timestamp formats, warn or escalate verdicts, or evaluation failures. Those inputs can differ between the Guardians. + +## Scope and parity + +| Component or behavior | This stage | TypeScript reference | +| --- | --- | --- | +| `POST /acs` JSON-RPC and handshake | Implemented | Implemented | +| `steps/toolCallRequest` and `steps/toolCallResult` | Implemented with AGT Rust SDK and the shared manifest | Implemented with AGT Node binding | +| Repository request and hook schema validation | Implemented | Implemented | +| Five AGT verdict mappings from `mapping.yaml` | Implemented; live tests cover allow, deny and transform | Implemented and covered by its mapping tests | +| Egress annotator and IFC source labels | Implemented; the Rust SDK cannot distinguish absent `result_labels` from an explicit empty array | Implemented | +| Session hash chain and envelope JSONL | Implemented locally; no `chain_hash` on the wire | Implemented locally; no `chain_hash` on the wire | +| Claude Code and OpenCode host shims | Use the existing TypeScript shims | Implemented | +| Host adapter, Inspector and conformance runner in Rust | Not yet ported | Implemented | + +Both Guardians have the TypeScript reference's stated limits: two of nineteen hooks are evaluated; replay checks, signatures, `system/ping`, and wrapped MCP are absent; `ask` lacks `ask_details`; the chain head is not published on responses; and the wire is unauthenticated. An `ask` response therefore fails the current response schema. This stage must not be used as a claim of full ACS-Core support. + +The pinned Rust AGT SDK represents `result_labels` as `Vec` and normalizes both a missing field and an explicit empty array to an empty vector. This Guardian retains the current session labels when the vector is empty. That preserves the prior restriction but can differ from the TypeScript Guardian when a policy explicitly clears labels. Full IFC parity needs an SDK API that preserves this distinction. + +The Rust source separates schema loading (`schema.rs`), AGT SDK integration and the egress annotator (`policy.rs`), mapping (`mapping.rs`), session state (`session.rs`), and HTTP dispatch and logs (`server.rs`). The policy files remain in `../agt/policy/`; none are duplicated here. diff --git a/reference-implementations/agt-rust/src/lib.rs b/reference-implementations/agt-rust/src/lib.rs new file mode 100644 index 0000000..a69f366 --- /dev/null +++ b/reference-implementations/agt-rust/src/lib.rs @@ -0,0 +1,9 @@ +//! Rust Guardian for the AGT ACS reference implementation. + +mod mapping; +mod policy; +mod schema; +mod server; +mod session; + +pub use server::{serve, Guardian, GuardianConfig}; diff --git a/reference-implementations/agt-rust/src/main.rs b/reference-implementations/agt-rust/src/main.rs new file mode 100644 index 0000000..891dd48 --- /dev/null +++ b/reference-implementations/agt-rust/src/main.rs @@ -0,0 +1,25 @@ +use acs_agt_guardian::{serve, GuardianConfig}; +use std::{env, net::SocketAddr, path::PathBuf}; + +#[tokio::main] +async fn main() -> Result<(), String> { + let mut config = GuardianConfig::from_repo(); + if let Some(value) = env::var_os("ACS_MANIFEST_PATH") { + config.manifest = PathBuf::from(value); + } + if let Some(value) = env::var_os("ACS_ENVELOPE_LOG") { + config.envelope_log = Some(PathBuf::from(value)); + } + if let Some(value) = env::var_os("ACS_SESSION_CONTEXT_LOG") { + config.session_log = Some(PathBuf::from(value)); + } + config.on_decision_failure = + env::var("ACS_ON_DECISION_FAILURE").unwrap_or_else(|_| "proceed".into()); + let host = env::var("ACS_GUARDIAN_HOST").unwrap_or_else(|_| "127.0.0.1".into()); + let port = env::var("ACS_GUARDIAN_PORT").unwrap_or_else(|_| "8787".into()); + let address: SocketAddr = format!("{host}:{port}") + .parse() + .map_err(|error| format!("invalid Guardian address: {error}"))?; + eprintln!("Rust Guardian listening at http://{address}/acs"); + serve(config, address).await +} diff --git a/reference-implementations/agt-rust/src/mapping.rs b/reference-implementations/agt-rust/src/mapping.rs new file mode 100644 index 0000000..1139051 --- /dev/null +++ b/reference-implementations/agt-rust/src/mapping.rs @@ -0,0 +1,341 @@ +use agent_control_specification::Verdict; +use regex::Regex; +use serde::Deserialize; +use serde_json::{json, Map, Value}; +use std::{collections::BTreeMap, fs, path::Path}; + +#[derive(Debug, Deserialize)] +pub(crate) struct Mapping { + acs_version: String, + intervention_points: BTreeMap, + verdicts: BTreeMap, + field_synthesis: Synthesis, +} + +#[derive(Debug, Deserialize)] +struct Point { + acs_method: Option, + policy_target_argument: Option, + modifications: Option, +} + +#[derive(Debug, Deserialize)] +struct TargetArgument { + default: String, + #[serde(default)] + by_tool: BTreeMap, +} + +#[derive(Debug, Deserialize)] +struct ModificationRule { + from: String, + when_path: String, + into: String, + into_path: Option, +} + +#[derive(Debug, Deserialize)] +struct VerdictRule { + decision: String, + #[serde(default)] + require_policy_references: bool, +} + +#[derive(Debug, Deserialize)] +struct Synthesis { + reasoning: Reasoning, + reason_codes: Source, + policy_references: PolicyReferences, +} + +#[derive(Debug, Deserialize)] +struct PolicyReferences { + rule_id: Source, + policy_id: Literal, +} + +#[derive(Debug, Deserialize)] +struct Source { + source: String, + wrap: Option, +} + +#[derive(Debug, Deserialize)] +struct Literal { + literal: String, +} + +#[derive(Debug, Deserialize)] +struct Reasoning { + source: String, + template: String, + summaries: BTreeMap, + detail: Detail, +} + +#[derive(Debug, Deserialize)] +struct Detail { + when_matches: String, + render: String, + otherwise: String, +} + +impl Mapping { + pub(crate) fn load(path: &Path) -> Result { + let content = + fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))?; + let mapping: Self = serde_yaml::from_str(&content).map_err(|error| error.to_string())?; + if mapping.acs_version != "0.1.0" + || mapping.field_synthesis.reasoning.source != "verdict.message" + || mapping.field_synthesis.reason_codes.source != "verdict.reason" + || mapping.field_synthesis.reason_codes.wrap.as_deref() != Some("array") + || mapping.field_synthesis.policy_references.rule_id.source != "verdict.reason" + { + return Err( + "mapping.yaml field synthesis differs from the supported AGT mapping".into(), + ); + } + for row in mapping.intervention_points.values() { + if row + .modifications + .as_ref() + .is_some_and(|rule| rule.from != "verdict.transform") + { + return Err("mapping.yaml names an unsupported transform source".into()); + } + } + for method in ["steps/toolCallRequest", "steps/toolCallResult"] { + mapping.point(method)?; + } + for decision in ["allow", "deny", "warn", "escalate", "transform"] { + if !mapping.verdicts.contains_key(decision) { + return Err(format!("mapping.yaml has no verdict rule for {decision}")); + } + } + Ok(mapping) + } + + pub(crate) fn point(&self, method: &str) -> Result<&str, String> { + let mut matches = self + .intervention_points + .iter() + .filter(|(_, row)| row.acs_method.as_deref() == Some(method)); + let first = matches + .next() + .ok_or_else(|| format!("mapping.yaml maps no AGT point to ACS method {method}"))?; + if matches.next().is_some() { + return Err(format!( + "mapping.yaml maps ACS method {method} more than once" + )); + } + Ok(first.0) + } + + pub(crate) fn target_argument(&self, point: &str, tool: &str) -> Option<&str> { + let row = self.intervention_points.get(point)?; + let table = row.policy_target_argument.as_ref()?; + Some(table.by_tool.get(tool).unwrap_or(&table.default)) + } + + pub(crate) fn map_verdict( + &self, + verdict: &Verdict, + point: &str, + target_argument: Option<&str>, + ) -> Result, String> { + let key = verdict.decision.as_str(); + let rule = self + .verdicts + .get(key) + .ok_or_else(|| format!("mapping.yaml has no verdict rule for {key}"))?; + let mut result = Map::new(); + result.insert("decision".into(), json!(rule.decision)); + + if let Some(reason) = verdict.reason.as_deref() { + let policy_id = &self.field_synthesis.policy_references.policy_id.literal; + result.insert("reason_codes".into(), json!([reason])); + result.insert( + "policy_references".into(), + json!([{"policy_id": policy_id, "rule_id": reason}]), + ); + result.insert( + "reasoning".into(), + json!(self.reasoning(reason, verdict.message.as_deref(), point)?), + ); + } else if rule.require_policy_references { + return Err(format!( + "AGT {key} verdict has no reason for required policy reference" + )); + } + + if rule.decision == "modify" { + let row = self + .intervention_points + .get(point) + .ok_or_else(|| format!("mapping.yaml has no point {point}"))?; + let modification = row + .modifications + .as_ref() + .ok_or_else(|| format!("mapping.yaml has no modifications rule for {point}"))?; + let transform = verdict + .transform + .as_ref() + .ok_or("AGT transform verdict has no transform")?; + if transform.path != modification.when_path { + return Err(format!( + "AGT transform path {} differs from mapping {}", + transform.path, modification.when_path + )); + } + let changes = match modification.into.as_str() { + "parameter_overrides" => { + let argument = target_argument.ok_or("mapping has no target argument")?; + let mut overrides = Map::new(); + overrides.insert(argument.to_owned(), transform.value.clone()); + json!({"parameter_overrides": overrides}) + } + "redactions" => { + let path = modification + .into_path + .as_deref() + .ok_or("mapping has no redaction path")?; + let value = transform + .value + .as_str() + .ok_or("AGT redaction value is not a string")?; + json!({"redactions": [{"path": path, "replacement": value}]}) + } + other => return Err(format!("unsupported mapping destination {other}")), + }; + result.insert("modifications".into(), changes); + } + Ok(result) + } + + fn reasoning( + &self, + reason: &str, + message: Option<&str>, + point: &str, + ) -> Result { + let config = &self.field_synthesis.reasoning; + let summary = config + .summaries + .get(reason) + .and_then(|value| { + value + .as_str() + .or_else(|| value.get(point).and_then(serde_yaml::Value::as_str)) + }) + .or_else(|| { + config + .summaries + .get("default") + .and_then(serde_yaml::Value::as_str) + }) + .unwrap_or(""); + let detail = if let Some(message) = message.filter(|text| !text.is_empty()) { + let regex = + Regex::new(&config.detail.when_matches).map_err(|error| error.to_string())?; + if let Some(captures) = regex.captures(message) { + config + .detail + .render + .replace("{1}", captures.get(1).map_or("", |hit| hit.as_str())) + } else { + config.detail.otherwise.replace("{message}", message) + } + } else { + String::new() + }; + Ok(config + .template + .replace("{summary}", summary) + .replace("{rule_id}", reason) + .replace( + "{policy_id}", + &self.field_synthesis.policy_references.policy_id.literal, + ) + .replace("{detail}", &detail)) + } +} + +#[cfg(test)] +mod tests { + use super::Mapping; + use agent_control_specification::{Decision, Transform, Verdict}; + use serde_json::json; + use std::path::PathBuf; + + fn mapping() -> Mapping { + Mapping::load(&PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../agt/mapping.yaml")) + .expect("shared mapping") + } + + fn verdict(decision: Decision) -> Verdict { + Verdict { + decision, + reason: Some("redaction_applied".into()), + message: None, + transform: None, + evidence: None, + result_labels: Vec::new(), + } + } + + #[test] + fn maps_warning_and_both_transform_destinations() { + let mapping = mapping(); + let warn = mapping + .map_verdict(&verdict(Decision::Warn), "pre_tool_call", Some("command")) + .expect("warn mapping"); + assert_eq!(warn["decision"], "allow"); + assert_eq!(warn["policy_references"][0]["rule_id"], "redaction_applied"); + + let escalate = mapping + .map_verdict( + &verdict(Decision::Escalate), + "pre_tool_call", + Some("command"), + ) + .expect("escalate mapping"); + assert_eq!(escalate["decision"], "ask"); + + let mut transformed = verdict(Decision::Transform); + transformed.transform = Some(Transform { + path: "$policy_target".into(), + value: json!("echo [REDACTED]"), + }); + let pre = mapping + .map_verdict(&transformed, "pre_tool_call", Some("command")) + .expect("request transform"); + assert_eq!( + pre["modifications"]["parameter_overrides"]["command"], + "echo [REDACTED]" + ); + let post = mapping + .map_verdict(&transformed, "post_tool_call", None) + .expect("result transform"); + assert_eq!( + post["modifications"]["redactions"][0]["path"], + "/outputs/0/value" + ); + assert_eq!( + post["modifications"]["redactions"][0]["replacement"], + "echo [REDACTED]" + ); + } + + #[test] + fn refuses_a_transform_that_mapping_cannot_apply() { + let mapping = mapping(); + let mut transformed = verdict(Decision::Transform); + transformed.transform = Some(Transform { + path: "$policy_target.other".into(), + value: json!("changed"), + }); + assert!(mapping + .map_verdict(&transformed, "pre_tool_call", Some("command")) + .is_err()); + } +} diff --git a/reference-implementations/agt-rust/src/policy.rs b/reference-implementations/agt-rust/src/policy.rs new file mode 100644 index 0000000..c9bb96f --- /dev/null +++ b/reference-implementations/agt-rust/src/policy.rs @@ -0,0 +1,142 @@ +use agent_control_specification::{ + AgentControl, AnnotatorDispatcher, AnnotatorInvocation, EnforcementMode, InterventionPoint, + JsonValue, OpaRegoRunner, RuntimeError, Verdict, +}; +use regex::Regex; +use serde_json::json; +use std::{path::Path, process::Command, sync::Arc, time::Duration}; +use url::Url; + +pub(crate) struct Policy { + runtime: AgentControl, + decision_timeout_ms: u64, +} + +const REQUIRED_OPA_VERSION: &str = "0.70.0"; + +fn check_opa_version(stdout: &[u8]) -> Result<(), String> { + let output = std::str::from_utf8(stdout).map_err(|_| "OPA version output is not UTF-8")?; + let version = output + .lines() + .find_map(|line| line.strip_prefix("Version:").map(str::trim)); + if version != Some(REQUIRED_OPA_VERSION) { + return Err(format!( + "OPA {REQUIRED_OPA_VERSION} is required by the pinned AGT bundle; found {}", + version.unwrap_or("no Version field") + )); + } + Ok(()) +} + +fn decision_timeout_ms(opa_timeout: Duration) -> Result { + u64::try_from(opa_timeout.as_millis()) + .ok() + .and_then(|millis| millis.checked_add(1000)) + .ok_or_else(|| "OPA timeout is too large to advertise a host deadline".into()) +} + +impl Policy { + pub(crate) fn load(manifest: &Path) -> Result { + // Use the SDK's path and timeout resolution, including its default timeout. + let runner = OpaRegoRunner::from_environment(); + let opa = runner.executable(); + let decision_timeout_ms = decision_timeout_ms(runner.eval_timeout())?; + let output = Command::new(opa) + .arg("version") + .output() + .map_err(|error| format!("cannot execute OPA at {}: {error}", opa.display()))?; + if !output.status.success() { + return Err(format!("OPA at {} failed its version check", opa.display())); + } + check_opa_version(&output.stdout)?; + let runtime = AgentControl::from_path_with_dispatchers( + manifest, + Some(Arc::new(EgressAnnotator)), + None, + ) + .map_err(|error| format!("AGT runtime: {error}"))?; + Ok(Self { + runtime, + decision_timeout_ms, + }) + } + + pub(crate) fn decision_timeout_ms(&self) -> u64 { + self.decision_timeout_ms + } + + pub(crate) fn evaluate(&self, point: &str, snapshot: JsonValue) -> Result { + let point: InterventionPoint = point.parse().map_err(|error: String| error)?; + Ok(self + .runtime + .evaluate_intervention_point(point, snapshot, EnforcementMode::Enforce) + .verdict) + } +} + +struct EgressAnnotator; + +impl AnnotatorDispatcher for EgressAnnotator { + fn dispatch( + &self, + _name: &str, + _config: &AnnotatorInvocation, + preliminary: &JsonValue, + ) -> Result { + let Some(tool) = preliminary.pointer("/snapshot/tool_call") else { + return Ok(json!({})); + }; + let args = &tool["args"]; + if ["url", "endpoint", "host", "domain"] + .iter() + .any(|key| args.get(key).is_some_and(JsonValue::is_string)) + { + return Ok(json!({})); + } + let Some(command) = tool["raw_command"].as_str() else { + return Ok(json!({})); + }; + let pattern = Regex::new(r##"\bhttps?://[^\s'"`;|&()<>]+"##) + .map_err(|error| RuntimeError::AnnotationFailed(error.to_string()))?; + let Some(hit) = pattern.find(command) else { + return Ok(json!({})); + }; + let candidate = hit.as_str(); + let authority = Regex::new(r"^https?://[A-Za-z0-9.\-]+(?::\d+)?(?:[/?#]|$)") + .map_err(|error| RuntimeError::AnnotationFailed(error.to_string()))?; + if !authority.is_match(candidate) { + return Ok(json!({"destination": "https://unresolved.invalid"})); + } + match Url::parse(candidate) { + Ok(url) => Ok(json!({"destination": url.origin().ascii_serialization()})), + Err(_) => Ok(json!({"destination": "https://unresolved.invalid"})), + } + } +} + +#[cfg(test)] +mod tests { + use super::{check_opa_version, decision_timeout_ms}; + use std::time::Duration; + + #[test] + fn requires_the_exact_pinned_opa_version() { + assert!(check_opa_version(b"Version: 0.70.0\r\nBuild Commit: abc\r\n").is_ok()); + for output in [ + b"Version: 1.0.0\n".as_slice(), + b"Version: 0.70.0-dev\n".as_slice(), + b"Build Commit: 0.70.0\n".as_slice(), + b"Version: 0.70.0\n\xff".as_slice(), + b"".as_slice(), + ] { + assert!(check_opa_version(output).is_err(), "{output:?}"); + } + } + + #[test] + fn host_deadline_leaves_a_second_for_delivery_after_opa() { + assert_eq!(decision_timeout_ms(Duration::from_secs(5)), Ok(6000)); + assert_eq!(decision_timeout_ms(Duration::from_secs(30)), Ok(31000)); + assert!(decision_timeout_ms(Duration::from_millis(u64::MAX)).is_err()); + } +} diff --git a/reference-implementations/agt-rust/src/schema.rs b/reference-implementations/agt-rust/src/schema.rs new file mode 100644 index 0000000..c37d8e9 --- /dev/null +++ b/reference-implementations/agt-rust/src/schema.rs @@ -0,0 +1,115 @@ +use jsonschema::{error::ValidationErrorKind, Draft, Resource, Validator}; +use serde_json::Value; +use std::{fs, path::Path}; + +pub(crate) struct Schemas { + request: Validator, + response: Validator, + tool_request: Validator, + tool_result: Validator, +} + +impl Schemas { + pub(crate) fn load(root: &Path) -> Result { + let mut resources = Vec::new(); + collect(root, &mut resources)?; + let build = |name: &str| -> Result { + let path = root.join(name); + let schema = read_json(&path)?; + let registered = resources + .iter() + .map(|(uri, value)| { + Resource::from_contents(value.clone()) + .map(|resource| (uri.clone(), resource)) + .map_err(|error| error.to_string()) + }) + .collect::, _>>()?; + jsonschema::options() + .with_draft(Draft::Draft202012) + .should_validate_formats(true) + .with_resources(registered.into_iter()) + .build(&schema) + .map_err(|error| format!("{}: {error}", path.display())) + }; + Ok(Self { + request: build("request-envelope.json")?, + response: build("response-envelope.json")?, + tool_request: build("hooks/tool-call-request.json")?, + tool_result: build("hooks/tool-call-result.json")?, + }) + } + + pub(crate) fn validate_request(&self, value: &Value) -> Result<(), String> { + check(&self.request, value, "")?; + match value.get("method").and_then(Value::as_str) { + Some("steps/toolCallRequest") => check( + &self.tool_request, + &value["params"]["payload"], + "/params/payload", + ), + Some("steps/toolCallResult") => check( + &self.tool_result, + &value["params"]["payload"], + "/params/payload", + ), + _ => Ok(()), + } + } + + pub(crate) fn validate_response(&self, value: &Value) -> Result<(), String> { + check(&self.response, value, "") + } +} + +fn check(validator: &Validator, value: &Value, prefix: &str) -> Result<(), String> { + if let Err(error) = validator.validate(value) { + let path = error.instance_path.to_string(); + let (pointer, detail) = match &error.kind { + ValidationErrorKind::Required { property } => { + let property = property.as_str().unwrap_or_default(); + ( + format!("{prefix}{path}/{property}"), + format!("must have required property '{property}'"), + ) + } + _ => ( + if path.is_empty() { + format!("{prefix}/") + } else { + format!("{prefix}{path}") + }, + error.to_string(), + ), + }; + Err(format!( + "ACS envelope failed schema validation at {pointer}: {detail}" + )) + } else { + Ok(()) + } +} + +fn read_json(path: &Path) -> Result { + let content = + fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))?; + serde_json::from_str(&content).map_err(|error| format!("{}: {error}", path.display())) +} + +fn collect(dir: &Path, out: &mut Vec<(String, Value)>) -> Result<(), String> { + for entry in fs::read_dir(dir).map_err(|error| format!("{}: {error}", dir.display()))? { + let entry = entry.map_err(|error| error.to_string())?; + let path = entry.path(); + if path.is_dir() { + collect(&path, out)?; + } else if path + .extension() + .is_some_and(|extension| extension == "json") + { + let value = read_json(&path)?; + if let Some(uri) = value.get("$id").and_then(Value::as_str) { + out.push((uri.to_owned(), value)); + } + } + } + Ok(()) +} diff --git a/reference-implementations/agt-rust/src/server.rs b/reference-implementations/agt-rust/src/server.rs new file mode 100644 index 0000000..f502df0 --- /dev/null +++ b/reference-implementations/agt-rust/src/server.rs @@ -0,0 +1,404 @@ +use crate::{mapping::Mapping, policy::Policy, schema::Schemas, session::SessionStore}; +use axum::{ + body::{to_bytes, Body}, + extract::State, + http::Request, + routing::post, + Json, Router, +}; +use chrono::Utc; +use serde_json::{json, Map, Value}; +use std::{ + fs::{self, File, OpenOptions}, + io::{self, Write}, + net::SocketAddr, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +const MAX_REQUEST_BYTES: usize = 1_048_576; + +/// Paths and the negotiated failure posture for the Rust Guardian. +pub struct GuardianConfig { + pub manifest: PathBuf, + pub mapping: PathBuf, + pub schemas: PathBuf, + pub envelope_log: Option, + pub session_log: Option, + pub on_decision_failure: String, +} + +impl GuardianConfig { + /// Use the policy, mapping, and schemas beside this repository's TypeScript reference. + pub fn from_repo() -> Self { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + Self { + manifest: root.join("../agt/policy/manifest.yaml"), + mapping: root.join("../agt/mapping.yaml"), + schemas: root.join("../../specification/v0.1.0"), + envelope_log: Some(root.join("../agt/.acs/envelopes-rust.jsonl")), + session_log: Some(root.join("../agt/.acs/session-context-rust.jsonl")), + on_decision_failure: "proceed".into(), + } + } +} + +/// A configured Guardian. Construct once and share its router across requests. +pub struct Guardian { + policy: Policy, + mapping: Mapping, + schemas: Schemas, + sessions: SessionStore, + envelope_log: Option>, + session_log: Option>>, + posture: String, +} + +struct EnvelopeLog { + file: Option, + seq: u64, +} + +impl Guardian { + /// Validate configuration and initialize AGT before accepting requests. + pub fn new(config: GuardianConfig) -> Result, String> { + if config.on_decision_failure != "proceed" && config.on_decision_failure != "deny" { + return Err("ACS_ON_DECISION_FAILURE must be proceed or deny".into()); + } + let policy = Policy::load(&config.manifest)?; + let mapping = Mapping::load(&config.mapping)?; + let schemas = Schemas::load(&config.schemas)?; + let envelope_log = config.envelope_log.map(|path| { + Mutex::new(EnvelopeLog { + file: open_log_best_effort(&path), + seq: 0, + }) + }); + let session_log = config + .session_log + .map(|path| Mutex::new(open_log_best_effort(&path))); + Ok(Arc::new(Self { + policy, + mapping, + schemas, + sessions: SessionStore::default(), + envelope_log, + session_log, + posture: config.on_decision_failure, + })) + } + + /// Build the same `POST /acs` route the TypeScript Guardian serves. + pub fn router(self: Arc) -> Router { + Router::new().route("/acs", post(handle)).with_state(self) + } + + fn respond(&self, raw: Value) -> Value { + let method = raw.get("method").and_then(Value::as_str).map(str::to_owned); + self.write_envelope("request", &raw, method.as_deref()); + let response = self.dispatch(&raw); + if let Err(error) = self.schemas.validate_response(&response) { + let _ = writeln!( + io::stderr().lock(), + "guardian sent a response that fails response-envelope.json: {error}" + ); + } + self.write_envelope("response", &response, method.as_deref()); + response + } + + fn dispatch(&self, raw: &Value) -> Value { + let id = rpc_id(raw); + if let Err(message) = self.schemas.validate_request(raw) { + if raw + .get("method") + .and_then(Value::as_str) + .is_some_and(|method| method.starts_with("steps/")) + { + if let Some(decision) = deny_on_failure(raw, &message, "envelope_invalid") { + return success(id, decision); + } + } + return failure(id, -32010, &message, None); + } + let method = raw["method"].as_str().unwrap_or_default(); + if method == "handshake/hello" { + return success( + id, + json!({ + "negotiated_version": "0.1.0", + "methods_evaluated": ["steps/toolCallRequest", "steps/toolCallResult"], + "selected_transport": "http", + "timeout_config": {"default_ms": self.policy.decision_timeout_ms()}, + "on_decision_failure": self.posture, + }), + ); + } + if method != "steps/toolCallRequest" && method != "steps/toolCallResult" { + return failure( + id, + -32011, + &format!("method not dispatched by this Guardian: {method}"), + Some(json!({"method": method})), + ); + } + match self.evaluate(raw) { + Ok(decision) => success(id, decision), + Err(message) => { + let message = sanitize(&message); + if let Some(decision) = deny_on_failure(raw, &message, "evaluation_failed") { + success(id, decision) + } else { + failure(id, -32020, &format!("evaluation failed: {message}"), None) + } + } + } + } + + fn evaluate(&self, raw: &Value) -> Result { + let method = raw["method"].as_str().ok_or("missing method")?; + let params = &raw["params"]; + let payload = ¶ms["payload"]; + let request_id = params["request_id"].as_str().ok_or("missing request_id")?; + let session_id = params["metadata"]["session_id"] + .as_str() + .ok_or("missing session_id")?; + let tool_name = payload["tool"]["name"] + .as_str() + .ok_or("missing tool name")?; + let (entry, labels) = self + .sessions + .append(session_id, method, request_id, tool_name)?; + self.write_session(&entry); + + let point = self.mapping.point(method)?; + let target_argument = self.mapping.target_argument(point, tool_name); + let budgets = + json!({"tool_call_count": 0, "token_count": 0, "elapsed_seconds": 0, "cost_usd": 0}); + let snapshot = if method == "steps/toolCallRequest" { + let target = target_argument.ok_or("mapping has no policy_target_argument")?; + let wrappers = payload["arguments"] + .as_object() + .ok_or("arguments is not an object")?; + let mut args = Map::new(); + for (name, wrapper) in wrappers { + args.insert(name.clone(), wrapper["value"].clone()); + } + if args.contains_key("acs_policy_target") { + return Err( + "tool argument acs_policy_target conflicts with Guardian policy target".into(), + ); + } + let value = args + .get(target) + .cloned() + .ok_or_else(|| format!("tool {tool_name} has no {target} argument"))?; + args.insert("acs_policy_target".into(), value); + json!({ + "envelope": {"budgets": budgets}, + "tool_call": { + "name": tool_name, + "args": args, + "id": request_id, + "raw_command": payload["raw_command"].as_str().unwrap_or("") + }, + "input": {"ifc": {"source_labels": labels}} + }) + } else { + let outputs = payload["outputs"] + .as_array() + .ok_or("outputs is not an array")? + .iter() + .map(|item| json!({"value": item["value"]})) + .collect::>(); + json!({ + "envelope": {"budgets": budgets}, + "tool_call": {"name": tool_name}, + "tool_result": {"outputs": outputs}, + "input": {"ifc": {"source_labels": labels}} + }) + }; + let verdict = self.policy.evaluate(point, snapshot)?; + let mut decision = self.mapping.map_verdict(&verdict, point, target_argument)?; + if !verdict.result_labels.is_empty() { + self.sessions + .replace_labels(session_id, &verdict.result_labels)?; + } + decision.insert("type".into(), json!("final")); + decision.insert("acs_version".into(), params["acs_version"].clone()); + decision.insert("request_id".into(), json!(request_id)); + Ok(Value::Object(decision)) + } + + fn write_envelope(&self, direction: &str, envelope: &Value, method: Option<&str>) { + let Some(log) = &self.envelope_log else { + return; + }; + let Ok(mut log) = log.lock() else { return }; + let entry = json!({ + "seq": log.seq + 1, + "recorded_at": timestamp(), + "direction": direction, + "method": method, + "rpc_id": rpc_id(envelope), + "envelope": envelope, + }); + let Some(file) = log.file.as_mut() else { + return; + }; + if let Err(error) = write_line(file, &entry) { + let _ = writeln!(io::stderr().lock(), "envelope log write failed: {error}"); + log.file = None; + } else { + log.seq += 1; + } + } + + fn write_session(&self, entry: &crate::session::ChainEntry) { + let Some(log) = &self.session_log else { return }; + let Ok(mut guard) = log.lock() else { return }; + let Some(file) = guard.as_mut() else { return }; + if let Err(error) = write_line(file, entry) { + let _ = writeln!( + io::stderr().lock(), + "session context log write failed: {error}" + ); + *guard = None; + } + } +} + +/// Serve the Guardian until the process is stopped. +pub async fn serve(config: GuardianConfig, addr: SocketAddr) -> Result<(), String> { + let guardian = Guardian::new(config)?; + let listener = tokio::net::TcpListener::bind(addr) + .await + .map_err(|error| error.to_string())?; + axum::serve(listener, guardian.router()) + .await + .map_err(|error| error.to_string()) +} + +async fn handle(State(guardian): State>, request: Request) -> Json { + let body = match to_bytes(request.into_body(), MAX_REQUEST_BYTES).await { + Ok(bytes) => bytes, + Err(_) => { + let response = failure( + Value::Null, + -32010, + "request body exceeds the 1048576-byte limit", + None, + ); + guardian.write_envelope("response", &response, None); + return Json(response); + } + }; + let raw: Value = match serde_json::from_slice(&body) { + Ok(raw) => raw, + Err(_) => { + let response = failure(Value::Null, -32700, "Parse error", None); + guardian.write_envelope("response", &response, None); + return Json(response); + } + }; + let id = rpc_id(&raw); + let method = raw.get("method").and_then(Value::as_str).map(str::to_owned); + let on_failure = Arc::clone(&guardian); + match tokio::task::spawn_blocking(move || guardian.respond(raw)).await { + Ok(response) => Json(response), + Err(error) => { + let response = failure( + id, + -32020, + &format!("guardian failed to handle the request: {error}"), + None, + ); + on_failure.write_envelope("response", &response, method.as_deref()); + Json(response) + } + } +} + +fn rpc_id(raw: &Value) -> Value { + match raw.get("id") { + Some(Value::String(id)) => json!(id), + Some(Value::Number(id)) => json!(id), + _ => Value::Null, + } +} + +fn success(id: Value, result: Value) -> Value { + json!({"jsonrpc": "2.0", "id": id, "result": result}) +} + +fn failure(id: Value, code: i64, message: &str, data: Option) -> Value { + if let Some(data) = data { + json!({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message, "data": data}}) + } else { + json!({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}}) + } +} + +fn deny_on_failure(raw: &Value, message: &str, reason: &str) -> Option { + if rpc_id(raw).is_null() { + return None; + } + let request_id = raw + .pointer("/params/request_id") + .and_then(Value::as_str) + .filter(|id| !id.is_empty()) + .map(str::to_owned) + .or_else(|| match rpc_id(raw) { + Value::String(id) => Some(id), + Value::Number(id) => Some(id.to_string()), + _ => None, + })?; + Some(json!({ + "type": "final", "acs_version": raw.pointer("/params/acs_version").and_then(Value::as_str).filter(|value| !value.is_empty()).unwrap_or("0.1.0"), + "request_id": request_id, + "decision": "deny", "reasoning": message, + "reason_codes": [reason], "policy_references": [] + })) +} + +fn timestamp() -> String { + Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true) +} + +fn sanitize(message: &str) -> String { + message.replace(env!("CARGO_MANIFEST_DIR"), "") +} + +fn open_log(path: &PathBuf) -> Result { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|error| format!("{}: {error}", parent.display()))?; + } + let mut options = OpenOptions::new(); + options.create(true).append(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + options + .open(path) + .map_err(|error| format!("{}: {error}", path.display())) +} + +fn open_log_best_effort(path: &PathBuf) -> Option { + match open_log(path) { + Ok(file) => Some(file), + Err(error) => { + let _ = writeln!( + io::stderr().lock(), + "log sink disabled after open failure: {error}" + ); + None + } + } +} + +fn write_line(file: &mut File, value: &impl serde::Serialize) -> Result<(), String> { + serde_json::to_writer(&mut *file, value).map_err(|error| error.to_string())?; + file.write_all(b"\n").map_err(|error| error.to_string()) +} diff --git a/reference-implementations/agt-rust/src/session.rs b/reference-implementations/agt-rust/src/session.rs new file mode 100644 index 0000000..af8daec --- /dev/null +++ b/reference-implementations/agt-rust/src/session.rs @@ -0,0 +1,211 @@ +use chrono::Utc; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::{ + collections::{HashMap, VecDeque}, + io::{self, Write}, + sync::Mutex, +}; + +#[derive(Clone)] +struct Session { + seq: u64, + hash: String, + labels: Vec, +} + +impl Default for Session { + fn default() -> Self { + Self { + seq: 0, + hash: "0".repeat(64), + labels: vec!["public".into()], + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct ChainEntry { + session_id: String, + seq: u64, + prev_hash: String, + hash: String, + recorded_at: String, + method: String, + request_id: String, + tool_name: String, +} + +#[derive(Default)] +struct Inner { + sessions: HashMap, + recent: VecDeque, +} + +const MAX_SESSIONS: usize = 1024; + +#[derive(Default)] +pub(crate) struct SessionStore(Mutex); + +fn ensure_session<'a>(inner: &'a mut Inner, session_id: &str) -> &'a mut Session { + inner.recent.retain(|id| id != session_id); + inner.recent.push_back(session_id.to_owned()); + + while inner.recent.len() > MAX_SESSIONS { + let Some(evicted) = inner.recent.pop_front() else { + break; + }; + inner.sessions.remove(&evicted); + warn_evicted_session(&evicted); + } + + inner.sessions.entry(session_id.to_owned()).or_default() +} + +fn warn_evicted_session(session_id: &str) { + let mut stderr = io::stderr().lock(); + let _ = writeln!( + stderr, + "evicted session {session_id} from the session-context store: the retained-session cap was reached. \ +A further step on this session starts a new chain at seq 1, which the Inspector renders as a chain break." + ); +} + +impl SessionStore { + pub(crate) fn append( + &self, + session_id: &str, + method: &str, + request_id: &str, + tool_name: &str, + ) -> Result<(ChainEntry, Vec), String> { + let mut inner = self.0.lock().map_err(|error| error.to_string())?; + let state = ensure_session(&mut inner, session_id); + let seq = state.seq + 1; + let recorded_at = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true); + let canonical = serde_json::to_vec(&( + &state.hash, + session_id, + seq, + &recorded_at, + method, + request_id, + tool_name, + )) + .map_err(|error| error.to_string())?; + let hash = format!("{:x}", Sha256::digest(canonical)); + let entry = ChainEntry { + session_id: session_id.to_owned(), + seq, + prev_hash: std::mem::replace(&mut state.hash, hash.clone()), + hash, + recorded_at, + method: method.to_owned(), + request_id: request_id.to_owned(), + tool_name: tool_name.to_owned(), + }; + state.seq = seq; + Ok((entry, state.labels.clone())) + } + + pub(crate) fn replace_labels(&self, session_id: &str, labels: &[String]) -> Result<(), String> { + let mut inner = self.0.lock().map_err(|error| error.to_string())?; + ensure_session(&mut inner, session_id).labels = labels.to_vec(); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::{SessionStore, MAX_SESSIONS}; + use sha2::{Digest, Sha256}; + use std::sync::Arc; + + #[test] + fn replace_labels_recreates_an_evicted_session_and_keeps_it_bounded() { + let store = SessionStore::default(); + store + .append("evicted", "steps/toolCallRequest", "initial", "Bash") + .expect("initial append"); + + for index in 0..1024 { + store + .append( + &format!("other-{index}"), + "steps/toolCallRequest", + &index.to_string(), + "Bash", + ) + .expect("fill session store"); + } + + { + let inner = store.0.lock().expect("session store lock"); + assert!(!inner.sessions.contains_key("evicted")); + assert_eq!(inner.sessions.len(), MAX_SESSIONS); + assert_eq!(inner.recent.len(), MAX_SESSIONS); + } + + let propagated = vec!["confidential".to_owned()]; + store + .replace_labels("evicted", &propagated) + .expect("replace labels"); + + let (entry, labels) = store + .append("evicted", "steps/toolCallResult", "after-eviction", "Bash") + .expect("append recreated session"); + + assert_eq!(entry.seq, 1); + assert_eq!(labels, propagated); + + let inner = store.0.lock().expect("session store lock"); + assert_eq!(inner.sessions.len(), MAX_SESSIONS); + assert_eq!(inner.recent.len(), MAX_SESSIONS); + assert!(inner.sessions.contains_key("evicted")); + } + + #[test] + fn appends_a_valid_chain_when_one_session_is_used_concurrently() { + let store = Arc::new(SessionStore::default()); + let entries = std::thread::scope(|scope| { + let jobs = (0..16) + .map(|index| { + let store = Arc::clone(&store); + scope.spawn(move || { + store + .append( + "session", + "steps/toolCallRequest", + &index.to_string(), + "Bash", + ) + .expect("append") + .0 + }) + }) + .collect::>(); + jobs.into_iter() + .map(|job| job.join().expect("thread")) + .collect::>() + }); + let mut entries = entries; + entries.sort_by_key(|entry| entry.seq); + let mut previous = "0".repeat(64); + for (index, entry) in entries.iter().enumerate() { + assert_eq!(entry.seq, index as u64 + 1); + assert_eq!(entry.prev_hash, previous); + let canonical = serde_json::to_vec(&( + &entry.prev_hash, + &entry.session_id, + entry.seq, + &entry.recorded_at, + &entry.method, + &entry.request_id, + &entry.tool_name, + )) + .expect("canonical entry"); + assert_eq!(entry.hash, format!("{:x}", Sha256::digest(canonical))); + previous.clone_from(&entry.hash); + } + } +} diff --git a/reference-implementations/agt-rust/tests/compare-guardians.mjs b/reference-implementations/agt-rust/tests/compare-guardians.mjs new file mode 100644 index 0000000..aa35a49 --- /dev/null +++ b/reference-implementations/agt-rust/tests/compare-guardians.mjs @@ -0,0 +1,87 @@ +// Run against live Rust and TypeScript Guardians using the same AGT manifest. +// Both URLs may be overridden for a local checkout. +import assert from "node:assert/strict"; +import { randomUUID } from "node:crypto"; + +const urls = [ + process.env.RUST_GUARDIAN_URL ?? "http://127.0.0.1:8787/acs", + process.env.TS_GUARDIAN_URL ?? "http://127.0.0.1:8788/acs", +]; +const sessionId = randomUUID(); + +function request(method, payload) { + const id = randomUUID(); + return { + jsonrpc: "2.0", + method, + id, + params: { + acs_version: "0.1.0", + request_id: id, + timestamp: "2026-09-07T16:00:37.962Z", + metadata: { agent_id: "claude-code", session_id: sessionId }, + payload, + }, + }; +} + +const cases = [ + ["handshake", request("handshake/hello", {})], + ["deny", request("steps/toolCallRequest", { + tool: { name: "Bash" }, + arguments: { command: { value: "echo rm -rf /" } }, + raw_command: "echo rm -rf /", + })], + ["allow", request("steps/toolCallRequest", { + tool: { name: "Bash" }, + arguments: { command: { value: "ls -la" } }, + raw_command: "ls -la", + })], + ["result redaction", request("steps/toolCallResult", { + tool: { name: "Bash" }, + exit_status: "success", + outputs: [{ value: "TOKEN=ghp_ABCDEF123456" }], + })], + ["egress deny", request("steps/toolCallRequest", { + tool: { name: "Bash" }, + arguments: { command: { value: "git clone https://github.com/openai/whisper" } }, + raw_command: "git clone https://github.com/openai/whisper", + })], + ["WebFetch allow", request("steps/toolCallRequest", { + tool: { name: "WebFetch" }, + arguments: { url: { value: "https://docs.anthropic.com/en/docs" } }, + })], + ["request redaction", request("steps/toolCallRequest", { + tool: { name: "Bash" }, + arguments: { command: { value: "echo ghp_ABCDEF123456" } }, + raw_command: "echo ghp_ABCDEF123456", + })], + ["invalid payload", request("steps/toolCallRequest", { tool: { name: "Bash" } })], + ["undispatched method", request("steps/sessionStart", {})], +]; + +for (const [name, input] of cases) { + const responses = await Promise.all(urls.map(async (url) => { + const response = await fetch(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(input), + }); + assert.equal(response.status, 200, `${name} at ${url} returned HTTP ${response.status}`); + return response.json(); + })); + if (name === "handshake") { + // Rust advertises the evaluator deadline plus delivery time. The TypeScript + // reference currently advertises a fixed five-second deadline. + for (const response of responses) { + assert.ok(Number.isSafeInteger(response.result.timeout_config.default_ms)); + assert.ok(response.result.timeout_config.default_ms > 0); + delete response.result.timeout_config.default_ms; + } + assert.deepStrictEqual(responses[0], responses[1], `${name} differs beyond default_ms`); + console.log(`${name}: equal except deployment-specific default_ms`); + } else { + assert.deepStrictEqual(responses[0], responses[1], `${name} differs`); + console.log(`${name}: equal`); + } +} diff --git a/reference-implementations/agt-rust/tests/guardian.rs b/reference-implementations/agt-rust/tests/guardian.rs new file mode 100644 index 0000000..70d6018 --- /dev/null +++ b/reference-implementations/agt-rust/tests/guardian.rs @@ -0,0 +1,227 @@ +use acs_agt_guardian::{Guardian, GuardianConfig}; +use serde_json::{json, Value}; + +fn request(method: &str, payload: Value, request_id: &str) -> Value { + json!({ + "jsonrpc": "2.0", + "method": method, + "id": request_id, + "params": { + "acs_version": "0.1.0", + "request_id": request_id, + "timestamp": "2026-09-07T16:00:37.962Z", + "metadata": { + "agent_id": "claude-code", + "session_id": "6ccc72b8-a167-5573-b3ea-a310262ea93f" + }, + "payload": payload + } + }) +} + +#[tokio::test] +async fn guardian_matches_the_two_tool_gates_and_refuses_invalid_steps() { + let mut config = GuardianConfig::from_repo(); + let logs = tempfile::tempdir().expect("temporary log directory"); + let envelope_path = logs.path().join("envelopes.jsonl"); + let session_path = logs.path().join("session-context.jsonl"); + config.envelope_log = Some(envelope_path.clone()); + config.session_log = Some(session_path.clone()); + let guardian = Guardian::new(config).expect("pinned AGT manifest should load"); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("loopback listener"); + let address = listener.local_addr().expect("listener address"); + let server = tokio::spawn(async move { axum::serve(listener, guardian.router()).await }); + let url = format!("http://{address}/acs"); + let client = reqwest::Client::new(); + let send = |value: Value| { + let client = client.clone(); + let url = url.clone(); + async move { + client + .post(url) + .json(&value) + .send() + .await + .expect("HTTP request") + .json::() + .await + .expect("JSON response") + } + }; + + let hello = send(request( + "handshake/hello", + json!({}), + "816407fa-7cf0-41d9-978e-398039d28dbe", + )) + .await; + assert_eq!( + hello["result"]["methods_evaluated"], + json!(["steps/toolCallRequest", "steps/toolCallResult"]) + ); + assert_eq!(hello["result"]["on_decision_failure"], "proceed"); + let opa_timeout = agent_control_specification::OpaRegoRunner::from_environment() + .eval_timeout() + .as_millis(); + assert_eq!( + hello["result"]["timeout_config"]["default_ms"], + json!(opa_timeout + 1000) + ); + + let denied = send(request( + "steps/toolCallRequest", + json!({ + "tool": {"name": "Bash"}, + "arguments": {"command": {"value": "echo rm -rf /"}}, + "raw_command": "echo rm -rf /" + }), + "6a22a0f7-5547-448a-add7-3327aed144df", + )) + .await; + assert_eq!(denied["result"]["decision"], "deny", "{denied}"); + assert_eq!( + denied["result"]["reason_codes"], + json!(["destructive_shell_command_blocked"]) + ); + + let allowed = send(request( + "steps/toolCallRequest", + json!({ + "tool": {"name": "Bash"}, + "arguments": {"command": {"value": "ls -la"}}, + "raw_command": "ls -la" + }), + "ff1c17fa-48e1-49dd-88c6-50796a504c14", + )) + .await; + assert_eq!(allowed["result"]["decision"], "allow", "{allowed}"); + + let fetch_allowed = send(request( + "steps/toolCallRequest", + json!({ + "tool": {"name": "WebFetch"}, + "arguments": {"url": {"value": "https://docs.anthropic.com/en/docs"}} + }), + "ccac4743-e2ef-4d31-95cf-0f55e417a790", + )) + .await; + assert_eq!( + fetch_allowed["result"]["decision"], "allow", + "{fetch_allowed}" + ); + + let request_redacted = send(request( + "steps/toolCallRequest", + json!({ + "tool": {"name": "Bash"}, + "arguments": {"command": {"value": "echo ghp_ABCDEF123456"}}, + "raw_command": "echo ghp_ABCDEF123456" + }), + "b361a722-7c7a-449f-930d-a52c1c8022ab", + )) + .await; + assert_eq!( + request_redacted["result"]["decision"], "modify", + "{request_redacted}" + ); + assert_eq!( + request_redacted["result"]["modifications"]["parameter_overrides"]["command"], + "echo [REDACTED]" + ); + + let egress_denied = send(request( + "steps/toolCallRequest", + json!({ + "tool": {"name": "Bash"}, + "arguments": {"command": {"value": "git clone https://github.com/openai/whisper"}}, + "raw_command": "git clone https://github.com/openai/whisper" + }), + "e30572e0-c599-407c-b369-af41345872c4", + )) + .await; + assert_eq!( + egress_denied["result"]["decision"], "deny", + "{egress_denied}" + ); + assert_eq!( + egress_denied["result"]["reason_codes"], + json!(["egress_destination_not_allowed"]) + ); + + let redacted = send(request( + "steps/toolCallResult", + json!({ + "tool": {"name": "Bash"}, + "exit_status": "success", + "outputs": [{"value": "TOKEN=ghp_ABCDEF123456"}] + }), + "928d63c8-453e-4a8f-b637-b6181632e69a", + )) + .await; + assert_eq!(redacted["result"]["decision"], "modify", "{redacted}"); + assert_eq!( + redacted["result"]["modifications"]["redactions"][0]["path"], + "/outputs/0/value" + ); + assert_eq!( + redacted["result"]["modifications"]["redactions"][0]["replacement"], + "TOKEN=[REDACTED]" + ); + + let invalid = send(request( + "steps/toolCallRequest", + json!({"tool": {"name": "Bash"}}), + "abc22c1b-6e7e-4212-9e90-acfded36f356", + )) + .await; + assert_eq!(invalid["result"]["decision"], "deny", "{invalid}"); + assert_eq!( + invalid["result"]["reason_codes"], + json!(["envelope_invalid"]) + ); + assert_eq!( + invalid["result"]["reasoning"], + "ACS envelope failed schema validation at /params/payload/arguments: must have required property 'arguments'" + ); + + let unknown = send(request( + "steps/sessionStart", + json!({}), + "278934b6-c5cd-45d7-81b5-5dc93b037e0d", + )) + .await; + assert_eq!(unknown["error"]["code"], -32011, "{unknown}"); + + let malformed: Value = client + .post(&url) + .body("{") + .send() + .await + .expect("HTTP request") + .json() + .await + .expect("JSON response"); + assert_eq!(malformed["error"]["code"], -32700); + + let oversized: Value = client + .post(&url) + .body("x".repeat(1_048_577)) + .send() + .await + .expect("HTTP request") + .json() + .await + .expect("JSON response"); + assert_eq!(oversized["error"]["code"], -32010); + + let envelopes = std::fs::read_to_string(envelope_path).expect("envelope log"); + assert!(envelopes.contains("echo rm -rf /")); + assert!(envelopes.contains("envelope_invalid")); + let chain = std::fs::read_to_string(session_path).expect("session log"); + assert!(chain.contains("steps/toolCallRequest")); + assert!(chain.contains("steps/toolCallResult")); + + server.abort(); +}