agent-toolprint 0.1.0

Double-signed receipts for AI-agent tool invocations — DSSE + JCS + Ed25519, verifiable offline (Rust port of @p-vbordei/agent-toolprint)
Documentation

agent-toolprint (Rust)

CI Spec License

Idiomatic Rust port of @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

Function What it does
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

cargo add agent-toolprint

Rust 2021 (stable). Async via tokio (or any executor).

Quickstart

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" });
}
cargo run --example quickstart
# envelope signatures : 2
# original            : PASS
# tampered            : FAIL (agent signature invalid)

How it relates

Implementation Repo Role
TypeScript agent-toolprint Reference. Owns SPEC + conformance vectors.
Python agent-toolprint-py Same wire format. Same vectors.
Rust this repo Same wire format. Same vectors.

Conformance

cargo test
# 3 tests, 15 vectors

Vectors under vectors/ are copied from the TS conformance suite. 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.

Development

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.