# agent-toolprint (Rust)
[](https://github.com/p-vbordei/agent-toolprint-rs/actions/workflows/ci.yml)
[](./SPEC.md)
[](./LICENSE)
> **Idiomatic Rust port of [@p-vbordei/agent-toolprint](https://github.com/p-vbordei/agent-toolprint).** Double-signed receipts for AI-agent tool invocations — DSSE envelope + RFC 8785 JCS canonical payload + Ed25519. Verifiable offline. Byte-deterministic-compatible with the TypeScript reference.
Answers one audit question: *"yes, agent X called tool Y with these args at T, both sides agree."* Agent signs. Tool counter-signs. Anyone with the public keys verifies — no host, no service, no chain.
## What's in the box
| `sign_agent(&receipt, &sk)` | Validate, JCS-canonicalize, return DSSE envelope with agent sig. |
| `countersign_tool(&env, &sk)` | Verify envelope is JCS-canonical, append the tool signature. |
| `verify(&env, &opts).await` | Run all SPEC §4 checks; optionally re-hash plaintext. |
| `chain(&parent, &child)` | True iff `child.parent == parent.id`. |
| `DidKeyResolver` | Bundled `did:key` impl of the `Resolver` async trait. |
| `did_key_from_ed25519_pubkey(pk)` / `parse_did_key(did)` | Multicodec `0xed01` round-trip. |
| `sha256_hash(value)` | SHA-256 over JCS-canonical bytes, `sha256:<hex>`. |
| `canonical(value)` | RFC 8785 canonical JSON bytes. |
| `validate_receipt` / `validate_envelope` | Strict schema validators. |
| `PAYLOAD_TYPE` / `PROTOCOL_VERSION` | DSSE `payloadType` and the `v` field. |
## Install
```bash
cargo add agent-toolprint
```
Rust 2021 (stable). Async via `tokio` (or any executor).
## Quickstart
```rust
use agent_toolprint::{
countersign_tool, did_key_from_ed25519_pubkey, sha256_hash, sign_agent, verify,
DidKeyResolver, VerifyOptions,
};
use base64::{engine::general_purpose::STANDARD, Engine};
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
use serde_json::json;
#[tokio::main]
async fn main() {
let agent_sk = SigningKey::generate(&mut OsRng);
let tool_sk = SigningKey::generate(&mut OsRng);
let agent_did = did_key_from_ed25519_pubkey(&agent_sk.verifying_key().to_bytes()).unwrap();
let tool_did = did_key_from_ed25519_pubkey(&tool_sk.verifying_key().to_bytes()).unwrap();
let args = json!({"query": "bun docs"});
let response = json!({"results": ["https://bun.sh/docs"]});
let receipt = json!({
"v": "tp/0.1",
"id": uuid::Uuid::new_v4().to_string(),
"ts": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"agent": {"did": agent_did, "key_id": "agent"},
"tool": {"did": tool_did, "key_id": "tool"},
"call": {"name": "search", "args_hash": sha256_hash(&args).unwrap()},
"result": {"status": "ok", "response_hash": sha256_hash(&response).unwrap()},
"nonce": STANDARD.encode([0u8; 32]),
});
let env = countersign_tool(
&sign_agent(&receipt, &agent_sk.to_bytes()).unwrap(),
&tool_sk.to_bytes(),
).unwrap();
let resolver = DidKeyResolver;
let res = verify(&env, &VerifyOptions::new(&resolver)).await;
println!("{}", if res.ok { "PASS" } else { "FAIL" });
}
```
```bash
cargo run --example quickstart
# envelope signatures : 2
# original : PASS
# tampered : FAIL (agent signature invalid)
```
## How it relates
| TypeScript | [`agent-toolprint`](https://github.com/p-vbordei/agent-toolprint) | Reference. Owns SPEC + conformance vectors. |
| Python | [`agent-toolprint-py`](https://github.com/p-vbordei/agent-toolprint-py) | Same wire format. Same vectors. |
| **Rust** | **this repo** | Same wire format. Same vectors. |
## Conformance
```bash
cargo test
# 3 tests, 15 vectors
```
Vectors under [`vectors/`](./vectors/) are copied from the [TS conformance suite](https://github.com/p-vbordei/agent-toolprint/tree/main/conformance/vectors). Coverage maps to SPEC §6:
- **C1** byte-identical canonical encoding (3 vectors).
- **C2** any field mutation fails verify (7 vectors).
- **C3** single-signed envelope rejected (2 vectors).
- **C4** `parent.id == child.parent` enforced (3 vectors).
## Architecture
See [docs/architecture.md](docs/architecture.md).
## Development
```bash
git clone https://github.com/p-vbordei/agent-toolprint-rs
cd agent-toolprint-rs
cargo test
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
```
## License
Apache-2.0 — see [LICENSE](./LICENSE).