Rust companion crate for GATE v1.4 (Governed Agent Trust Environment).
Per GATE v1.4 paper-update 10 line 18, gate-rust is the high-throughput Tool Gateway companion to gate-python. It ships four surfaces: canonical JSON, envelopes, ledger, and signing.
gate-rust v1.0.0 pairs with gate-python v1.2.0. The canonical-JSON test vectors at gate-python/gate/test_vectors/canonical_json_vectors.json are the cross-language compatibility source of truth. gate-rust's tests/canonical_json_vectors.rs drift guard reads these vectors at every test run; the build script reads them at every cargo build; both fail loudly on byte-level drift.
Forward compatibility: gate-rust v1.0.0 also accepts vectors from any later gate-python minor version whose canonical-JSON algorithm has not changed. If a future gate-python release breaks byte equivalence, gate-rust v1.1 ships with the new algorithm and the cross-version pairing table is updated.
gate-rust's canonical_json replicates gate-python's gate.hashing.canonical_json behaviour exactly, which is a pragmatic subset of RFC 8785 (sort_keys=True, separators=(",", ":"), ensure_ascii=False). It is NOT a full RFC 8785 implementation; consumers needing strict RFC 8785 elsewhere in their stack should not assume equivalence.
The thirteen reference vectors are the byte-level contract. Empirical verification at v1.0.0 cut confirmed all thirteen round-trip byte-for-byte with serde_json defaults plus recursive key sort. No custom number-formatting layer is needed for gate-python compatibility.
gate-rust v1.0.0 bundles 15 JSON Schema files matching gate-python v1.2.0's pinned schema set. The bundled src/schemas/MANIFEST.yaml structurally includes 6 additional entries marked PENDING_AT_RELEASE, deferred to gate-rust v1.1 alongside gate-python v1.3.0; these are not listable via list_schemas() or loadable via load_schema() in v1.0.0. The total of 21 manifest entries matches gate-python's manifest entry-for-entry: each entry's schema name and the sha256 hash recorded against it are identical across the two manifests. The MANIFEST.yaml files themselves are not byte-identical (the gate-python and gate-rust generators write YAML with slightly different field ordering and indentation), but the hash content the manifests certify matches across the two languages.
The build script verifies bundled schemas hash to their MANIFEST sha256s at every cargo build. The CI drift guard at tests/schema_manifest_drift.rs separately verifies the bundled set against the gate-contracts source-of-truth when monorepo-adjacent.
Minimum supported Rust version: 1.78. MSRV upgrades happen in minor releases only, never in patch releases.
| Feature | Default | Purpose |
|---|---|---|
canonical-json |
on | Canonical-JSON serialisation + SHA-256 hashing. Foundation; always available. |
schemas |
on | Bundled JSON Schema files via gate_rust::schemas::load_schema / list_schemas. |
envelopes |
on | ToolRequestEnvelope + ToolResponseEnvelope builders. Pulls serde, time, uuid. |
ledger |
on | LedgerEvent + LedgerChain + verify_chain. Pulls serde, time, uuid. |
signing |
on | ES256 sign + verify + KeyRegistry. Pulls base64, ring. |
Downstream consumers wanting canonical-JSON only can opt out via default-features = false, features = ["canonical-json"]. The minimal feature set has been verified to compile and test cleanly (15 tests, no serde / time / uuid / ring dependencies pulled).
A xtask feature gates the maintainer-only regenerate_schema_manifest binary; downstream consumers do not enable it.
Two signing-module behaviours match gate-python's contract rather than Rust's typical error-handling style. Both are deliberate; cross-language compatibility takes precedence over idiom here.
verify_signature(payload, record, public_key) -> Result<bool>returnsOk(false)for an invalid signature OR malformed base64.Erris reserved for unsupported algorithm strings only. Mirrors gate-pythonsigning.py:204-271("never raises on invalid signature").KeyRegistry::verify(payload, record) -> Result<bool>returnsOk(false)when thesigning_key_idis not registered.Erris not returned for missing keys. Mirrors gate-pythonKeyRegistry.verify.
Consumers expecting an Err on missing key or invalid signature should wrap with their own adapter.
gate-rust v1.0.0 returns PKCS8 DER bytes from SigningKey::generate and consumes PKCS8 DER in SigningKey::from_pkcs8. PEM wrapping is mechanical: base64-encode the DER + add -----BEGIN PRIVATE KEY----- / -----END PRIVATE KEY----- headers + wrap at 64 chars. PEM helpers are a v1.1 candidate if downstream demand surfaces.
For public keys, gate-rust v1.0.0 returns SEC1 uncompressed bytes (0x04 || X || Y, 65 bytes for P-256) from SigningKey::public_key_bytes. SPKI / PEM wrapping is similarly mechanical for now.
ES256 signing uses ring 0.17 with ECDSA_P256_SHA256_ASN1_SIGNING for signing and ECDSA_P256_SHA256_ASN1 for verification. Signatures are ASN.1 DER encoded, matching gate-python's cryptography library default. The fixed-width 64-byte format (ring's _FIXED variant) is NOT used and would break cross-language signature interpretation.
ring stagnation or fork is a known ecosystem risk. Documented swap candidates for a future minor release: aws-lc-rs (drop-in for ring's API surface), p256 + ecdsa (pure-Rust RustCrypto suite). Neither is shipped in v1.0.0; revisit at v1.1 if downstream demand surfaces.
The CI workflow (.github/workflows/ci.yml) runs cargo test --locked --all-features, cargo clippy --locked --all-features --all-targets -- -D warnings, and a non-skippable vector-file digest assertion. cargo test and
cargo build in turn exercise the drift guards below.
Build-time (cargo build fails on drift):
- Bundled
canonical_json_vectors.jsonmatches the gate-python source-of-truth (skip-on-missing when the monorepo-adjacent source is absent). - Bundled schemas hash to their MANIFEST.yaml entries (always active; no skip path).
Test-time (cargo test fails on drift):
- All 14 positive canonical-JSON vectors round-trip byte + sha256 equal (skip-on-missing when the vector file is absent, which cannot happen in this repo because the copy is bundled).
- The negative vectors (exponent-range floats) normalise via
round4to the cross-language-agreed canonical bytes and hash, and each raw serialisation matches the recorded Rust output. - Bundled schemas hash-equivalent to gate-contracts source for overlapping schemas (skip-on-missing when standalone).
The skip-on-missing pattern (guards 1, 3, 5) follows the W5 precedent
(test_reason_codes_present_in_rego_policy): these guards fail loudly when their
cross-workstream input is present and diverges, and print a skip notice when it
is absent (standalone consumer install).
Because both repos publish standalone, the vector file exists as a committed copy
in each. The non-skippable cross-repo identity check lives in CI: both
gate-rust and gate-python assert the bundled canonical_json_vectors.json
matches a checked-in canonical_json_vectors.sha256. The two repos carry an
identical digest; when the vectors change, update the .sha256 file in both
repos in the same change.
use gate_rust::canonical_json::gate_hash;
use gate_rust::envelopes::{
Decision, Environment, RiskTier, Status, ToolCategory,
ToolRequestBuilder, ToolResponseBuilder,
};
use gate_rust::ledger::{
build_event, verify_chain, ActionType, BuildEventParams,
RetentionClass, GENESIS,
};
use gate_rust::signing::{sign_event_hash, KeyRegistry, SigningKey};
use serde_json::json;
# fn main() -> Result<(), Box<dyn std::error::Error>> {
let request = ToolRequestBuilder::new()
.run_id("11111111-1111-1111-1111-111111111111")
.trace_id("trace-1")
.tenant_id("acme")
.environment(Environment::Prod)
.agent("spiffe://org/agent/treasury#run-1",
"treasury-agent", "2.1.0", true)
.tool("transfer_funds", ToolCategory::Financial, RiskTier::High)
.payload(json!({"amount": 500, "to": "acct-2"}))
.policy_bundle_hash(format!("sha256:{}", "a".repeat(64)))
.tool_schema_hash(format!("sha256:{}", "b".repeat(64)))
.build()?;
let response = ToolResponseBuilder::new()
.request_envelope(request.clone())
.tool_output(json!({"transfer_id": "tx-1"}))
.status(Status::Success)
.duration_ms(217)
.decision_id("dec-1")
.decision(Decision::Allow)
.obligations(vec!["audit_log".to_string()])
.policy_bundle_hash(request.bundles.policy_bundle_hash.clone())
.ledger_event_id("led-1")
.build()?;
let event = build_event(BuildEventParams {
run_id: request.run_id.clone(),
tenant_id: request.tenant_id.clone(),
environment: "prod".to_string(),
action_type: ActionType::ToolInvoke,
policy_decision_id: response.policy.decision_id.clone(),
tool_request_hash: request.hashes.request_hash.clone(),
tool_response_hash: response.hashes.response_hash.clone(),
prev_event_hash: GENESIS.to_string(),
sink_uri: "worm://acme/audit/prod/".to_string(),
retention_class: RetentionClass::ProdHot365d,
trace_id: Some(request.trace_id.clone()),
replay_trace_step_id: None,
hitl_approval_id: None,
invariant_bundle_hash: None,
tool_name: Some(request.tool.name.clone()),
agent_instance_id: Some(request.agent.agent_instance_id.clone()),
sequence_number: Some(1),
});
let (key, _pkcs8) = SigningKey::generate()?;
let mut registry = KeyRegistry::new();
registry.register_private("kid-treasury-2026", &key);
let signature = sign_event_hash(
&event.hash_chain.event_hash,
&key,
"kid-treasury-2026",
)?;
assert!(verify_chain(&[event.clone()], GENESIS).passed);
let public_key = key.public_key_bytes();
assert!(gate_rust::signing::verify_event_hash_signature(
&event.hash_chain.event_hash,
&signature,
&public_key,
)?);
# Ok(())
# }v1.4 deliberately defers crates.io publication. Operators install from a tagged git ref:
cargo add gate-rust --git https://github.com/deterministic-agents/gate-rust --tag v1.0.0
gate-rust v1.0.0 is released as part of the coordinated GATE v1.4 framework release (paper-update 10). It pairs with gate-python v1.2.0 and gate-contracts v1.2.0. See CHANGELOG-v1.0.0.md for the full release notes and BUNDLE-REVIEW.md for the bundle review summary.
MIT. See LICENSE.