From 2a80614a0f009298a2e7cc94ff2efa3ccbb17622 Mon Sep 17 00:00:00 2001 From: Arham Begani Date: Fri, 25 Sep 2026 19:25:24 +0530 Subject: [PATCH 1/7] refimpl: add first-stage Rust AGT Guardian Signed-off-by: Arham Begani --- .../workflows/reference-implementation.yml | 37 + LICENSING.md | 1 + reference-implementations/agt-rust/.gitignore | 1 + reference-implementations/agt-rust/Cargo.lock | 2953 +++++++++++++++++ reference-implementations/agt-rust/Cargo.toml | 24 + reference-implementations/agt-rust/README.md | 64 + reference-implementations/agt-rust/src/lib.rs | 9 + .../agt-rust/src/main.rs | 25 + .../agt-rust/src/mapping.rs | 341 ++ .../agt-rust/src/policy.rs | 90 + .../agt-rust/src/schema.rs | 115 + .../agt-rust/src/server.rs | 395 +++ .../agt-rust/src/session.rs | 151 + .../agt-rust/tests/compare-guardians.mjs | 75 + .../agt-rust/tests/guardian.rs | 220 ++ 15 files changed, 4501 insertions(+) create mode 100644 reference-implementations/agt-rust/.gitignore create mode 100644 reference-implementations/agt-rust/Cargo.lock create mode 100644 reference-implementations/agt-rust/Cargo.toml create mode 100644 reference-implementations/agt-rust/README.md create mode 100644 reference-implementations/agt-rust/src/lib.rs create mode 100644 reference-implementations/agt-rust/src/main.rs create mode 100644 reference-implementations/agt-rust/src/mapping.rs create mode 100644 reference-implementations/agt-rust/src/policy.rs create mode 100644 reference-implementations/agt-rust/src/schema.rs create mode 100644 reference-implementations/agt-rust/src/server.rs create mode 100644 reference-implementations/agt-rust/src/session.rs create mode 100644 reference-implementations/agt-rust/tests/compare-guardians.mjs create mode 100644 reference-implementations/agt-rust/tests/guardian.rs diff --git a/.github/workflows/reference-implementation.yml b/.github/workflows/reference-implementation.yml index 424d2a97..a2cbc30e 100644 --- a/.github/workflows/reference-implementation.yml +++ b/.github/workflows/reference-implementation.yml @@ -76,3 +76,40 @@ jobs: # schemas in this repository rather than a copy of them. - name: Run the tests run: bun test + + agt-rust: + runs-on: ubuntu-latest + 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 AGT's pinned OPA binary + run: | + npm install --prefix "$RUNNER_TEMP/agt-opa" --no-save agent-control-specification-opa-linux-x64@0.3.1-beta.0 + echo "ACS_OPA_PATH=$RUNNER_TEMP/agt-opa/node_modules/agent-control-specification-opa-linux-x64/bin/opa" >> "$GITHUB_ENV" + + - 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 diff --git a/LICENSING.md b/LICENSING.md index de5e29c7..4a844c0f 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -19,6 +19,7 @@ Copyright 2025-2026 The OWASP GenAI Security Project and the ACS contributors. | `design/**` | CC BY-SA 4.0 | `CC-BY-SA-4.0` | | `landing/assets/fonts/**` | SIL Open Font License 1.1 | `OFL-1.1` | | `reference-implementations/agt/policy/lib/**` | MIT License, Copyright (c) Microsoft Corporation | `MIT` | +| `reference-implementations/agt-rust/**/*.md` | CC BY-SA 4.0 | `CC-BY-SA-4.0` | | Everything else, including any new top-level directory | Apache License 2.0 | `Apache-2.0` | A more specific row wins over the catch-all. The catch-all exists so a directory added later is governed the day it lands rather than on the day someone notices it was never listed. Apache 2.0 is the default because a new directory is usually code, and an over-permissive grant on prose costs less than a ShareAlike obligation attaching by accident to reference code that adopters copy into their own systems. When a new directory holds prose, add an explicit `CC-BY-SA-4.0` row for it in the same pull request that creates it. diff --git a/reference-implementations/agt-rust/.gitignore b/reference-implementations/agt-rust/.gitignore new file mode 100644 index 00000000..b83d2226 --- /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 00000000..39c6533a --- /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 00000000..bbe34134 --- /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 00000000..9480561e --- /dev/null +++ b/reference-implementations/agt-rust/README.md @@ -0,0 +1,64 @@ +# 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). +- An `opa` executable compatible with the pinned AGT policy bundle. Install OPA or set `ACS_OPA_PATH` to its executable path. Construction checks that OPA starts, then loads the AGT manifest. Local tests used the AGT SDK's bundled OPA 0.70.0. +- 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. + +## 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 +``` + +The integration test starts a real HTTP 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 requires OPA. The test command gives slow test machines a longer OPA subprocess deadline; the server's default AGT deadline remains five seconds. 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. It sends nine identical requests to both servers and fails on any JSON response difference. `RUST_GUARDIAN_URL` and `TS_GUARDIAN_URL` can override the endpoints. + +## 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 00000000..a69f3665 --- /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 00000000..891dd486 --- /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 00000000..1139051a --- /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 00000000..4e96bc8a --- /dev/null +++ b/reference-implementations/agt-rust/src/policy.rs @@ -0,0 +1,90 @@ +use agent_control_specification::{ + AgentControl, AnnotatorDispatcher, AnnotatorInvocation, EnforcementMode, InterventionPoint, + JsonValue, RuntimeError, Verdict, +}; +use regex::Regex; +use serde_json::json; +use std::{path::Path, process::Command, sync::Arc}; +use url::Url; + +pub(crate) struct Policy { + runtime: AgentControl, +} + +impl Policy { + pub(crate) fn load(manifest: &Path) -> Result { + // The pinned AGT runtime invokes OPA as a subprocess. Check this at boot, + // so a missing evaluator cannot silently turn every decision into a deny. + let opa = std::env::var_os("ACS_OPA_PATH").unwrap_or_else(|| "opa".into()); + let output = Command::new(&opa) + .arg("version") + .output() + .map_err(|error| { + format!( + "cannot execute OPA at {}: {error}", + Path::new(&opa).display() + ) + })?; + if !output.status.success() { + return Err(format!( + "OPA at {} failed its version check", + Path::new(&opa).display() + )); + } + let runtime = AgentControl::from_path_with_dispatchers( + manifest, + Some(Arc::new(EgressAnnotator)), + None, + ) + .map_err(|error| format!("AGT runtime: {error}"))?; + Ok(Self { runtime }) + } + + 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"})), + } + } +} diff --git a/reference-implementations/agt-rust/src/schema.rs b/reference-implementations/agt-rust/src/schema.rs new file mode 100644 index 00000000..c37d8e99 --- /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 00000000..65c2d224 --- /dev/null +++ b/reference-implementations/agt-rust/src/server.rs @@ -0,0 +1,395 @@ +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::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) { + eprintln!("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": 5000}, + "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) { + eprintln!("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) { + eprintln!("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) => { + eprintln!("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 00000000..fb838d86 --- /dev/null +++ b/reference-implementations/agt-rust/src/session.rs @@ -0,0 +1,151 @@ +use chrono::Utc; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::{ + collections::{HashMap, VecDeque}, + 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, +} + +#[derive(Default)] +pub(crate) struct SessionStore(Mutex); + +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())?; + inner.recent.retain(|id| id != session_id); + inner.recent.push_back(session_id.to_owned()); + if inner.recent.len() > 1024 { + if let Some(evicted) = inner.recent.pop_front() { + inner.sessions.remove(&evicted); + eprintln!("evicted session {evicted}: retained-session cap reached"); + } + } + let state = inner.sessions.entry(session_id.to_owned()).or_default(); + 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())?; + if let Some(state) = inner.sessions.get_mut(session_id) { + state.labels = labels.to_vec(); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::SessionStore; + use sha2::{Digest, Sha256}; + use std::sync::Arc; + + #[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 00000000..af8aac4d --- /dev/null +++ b/reference-implementations/agt-rust/tests/compare-guardians.mjs @@ -0,0 +1,75 @@ +// 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(); + })); + 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 00000000..f069926e --- /dev/null +++ b/reference-implementations/agt-rust/tests/guardian.rs @@ -0,0 +1,220 @@ +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 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(); +} From fa5eec382be63ab04b04f8dba9a0f63d34507007 Mon Sep 17 00:00:00 2001 From: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:25:33 +0530 Subject: [PATCH 2/7] ci: run live Rust and TypeScript Guardian parity check Signed-off-by: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> --- .../workflows/reference-implementation.yml | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/.github/workflows/reference-implementation.yml b/.github/workflows/reference-implementation.yml index a2cbc30e..9230ead7 100644 --- a/.github/workflows/reference-implementation.yml +++ b/.github/workflows/reference-implementation.yml @@ -113,3 +113,54 @@ jobs: env: ACS_OPA_TIMEOUT_MS: "30000" run: cargo test --workspace --all-features --locked + + - name: Install Bun for live parity test + 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 + + - 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 From e1e17e06fd59d475dbe2b7dd03b43c0d20e7088e Mon Sep 17 00:00:00 2001 From: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:25:43 +0530 Subject: [PATCH 3/7] docs: note automated cross-language parity check Signed-off-by: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> --- reference-implementations/agt-rust/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference-implementations/agt-rust/README.md b/reference-implementations/agt-rust/README.md index 9480561e..0a87a85c 100644 --- a/reference-implementations/agt-rust/README.md +++ b/reference-implementations/agt-rust/README.md @@ -42,7 +42,7 @@ ACS_OPA_PATH=/absolute/path/to/opa ACS_OPA_TIMEOUT_MS=30000 cargo test --workspa The integration test starts a real HTTP 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 requires OPA. The test command gives slow test machines a longer OPA subprocess deadline; the server's default AGT deadline remains five seconds. 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. It sends nine identical requests to both servers and fails on any JSON response difference. `RUST_GUARDIAN_URL` and `TS_GUARDIAN_URL` can override the endpoints. +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. It sends nine identical requests to both servers and fails on any JSON response difference. `RUST_GUARDIAN_URL` and `TS_GUARDIAN_URL` can override the endpoints. CI also starts both Guardians and runs this comparison on every relevant pull request, so cross-language response drift fails the `Reference implementation` workflow rather than relying on a manual check. ## Scope and parity From 09bc0ef0609b39a39709535e7d91406c4c4fd896 Mon Sep 17 00:00:00 2001 From: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:39:34 +0530 Subject: [PATCH 4/7] fix(rust): preserve IFC labels across session eviction Signed-off-by: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> --- .../agt-rust/src/session.rs | 87 ++++++++++++++++--- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/reference-implementations/agt-rust/src/session.rs b/reference-implementations/agt-rust/src/session.rs index fb838d86..6948597f 100644 --- a/reference-implementations/agt-rust/src/session.rs +++ b/reference-implementations/agt-rust/src/session.rs @@ -41,9 +41,29 @@ struct Inner { 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); + eprintln!( + "evicted session {evicted} 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." + ); + } + + inner.sessions.entry(session_id.to_owned()).or_default() +} + impl SessionStore { pub(crate) fn append( &self, @@ -53,15 +73,7 @@ impl SessionStore { tool_name: &str, ) -> Result<(ChainEntry, Vec), String> { let mut inner = self.0.lock().map_err(|error| error.to_string())?; - inner.recent.retain(|id| id != session_id); - inner.recent.push_back(session_id.to_owned()); - if inner.recent.len() > 1024 { - if let Some(evicted) = inner.recent.pop_front() { - inner.sessions.remove(&evicted); - eprintln!("evicted session {evicted}: retained-session cap reached"); - } - } - let state = inner.sessions.entry(session_id.to_owned()).or_default(); + 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(&( @@ -91,9 +103,7 @@ impl SessionStore { 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())?; - if let Some(state) = inner.sessions.get_mut(session_id) { - state.labels = labels.to_vec(); - } + ensure_session(&mut inner, session_id).labels = labels.to_vec(); Ok(()) } } @@ -104,6 +114,59 @@ mod tests { 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()); From 63bdf71e74544b2382b230462dad8209d6cca854 Mon Sep 17 00:00:00 2001 From: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:39:53 +0530 Subject: [PATCH 5/7] test(rust): import session cap in eviction regression Signed-off-by: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> --- reference-implementations/agt-rust/src/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference-implementations/agt-rust/src/session.rs b/reference-implementations/agt-rust/src/session.rs index 6948597f..98799289 100644 --- a/reference-implementations/agt-rust/src/session.rs +++ b/reference-implementations/agt-rust/src/session.rs @@ -110,7 +110,7 @@ impl SessionStore { #[cfg(test)] mod tests { - use super::SessionStore; + use super::{SessionStore, MAX_SESSIONS}; use sha2::{Digest, Sha256}; use std::sync::Arc; From 214c3e51d5cd2c990eb251547363ef164b21c2c8 Mon Sep 17 00:00:00 2001 From: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:42:45 +0530 Subject: [PATCH 6/7] style(rust): apply rustfmt to eviction regression Signed-off-by: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> --- reference-implementations/agt-rust/src/session.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/reference-implementations/agt-rust/src/session.rs b/reference-implementations/agt-rust/src/session.rs index 98799289..afbd2656 100644 --- a/reference-implementations/agt-rust/src/session.rs +++ b/reference-implementations/agt-rust/src/session.rs @@ -118,12 +118,7 @@ mod tests { fn replace_labels_recreates_an_evicted_session_and_keeps_it_bounded() { let store = SessionStore::default(); store - .append( - "evicted", - "steps/toolCallRequest", - "initial", - "Bash", - ) + .append("evicted", "steps/toolCallRequest", "initial", "Bash") .expect("initial append"); for index in 0..1024 { @@ -150,12 +145,7 @@ mod tests { .expect("replace labels"); let (entry, labels) = store - .append( - "evicted", - "steps/toolCallResult", - "after-eviction", - "Bash", - ) + .append("evicted", "steps/toolCallResult", "after-eviction", "Bash") .expect("append recreated session"); assert_eq!(entry.seq, 1); From 6df8f8c00f90dda90e0d1710d65dd5c15ba27d20 Mon Sep 17 00:00:00 2001 From: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:43:53 +0530 Subject: [PATCH 7/7] fix(rust): make eviction reporting non-panicking Signed-off-by: Arham Begani <123586002+Arham-Begani@users.noreply.github.com> --- reference-implementations/agt-rust/src/session.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/reference-implementations/agt-rust/src/session.rs b/reference-implementations/agt-rust/src/session.rs index afbd2656..af8daec4 100644 --- a/reference-implementations/agt-rust/src/session.rs +++ b/reference-implementations/agt-rust/src/session.rs @@ -3,6 +3,7 @@ use serde::Serialize; use sha2::{Digest, Sha256}; use std::{ collections::{HashMap, VecDeque}, + io::{self, Write}, sync::Mutex, }; @@ -55,15 +56,21 @@ fn ensure_session<'a>(inner: &'a mut Inner, session_id: &str) -> &'a mut Session break; }; inner.sessions.remove(&evicted); - eprintln!( - "evicted session {evicted} 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." - ); + 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,