View on GitHub →
← Back to Home

Rust: Tool Contracts with ToolClad

An interactive guide to defining .clad.toml manifests, validating typed arguments, and executing tools safely with ToolClad v0.5.

The Problem: Unconstrained Tool Execution

Agentic runtimes let LLMs invoke CLI tools, but without constraints the model can generate arbitrary shell commands. Deny-list approaches are fragile — one missed metacharacter and you have command injection.

ToolClad inverts the model: instead of blocking dangerous commands, it constrains the LLM to fill typed parameters validated against a declarative manifest (allow-list). The dangerous action cannot be expressed because the interface doesn't permit it.

  • Typed parameters with automatic validation (string, integer, port, enum, scope_target, url, path, ip_address, cidr, boolean)
  • Shell metacharacter rejection by default on all string types
  • Command template rendering with value mappings
  • Evidence envelopes with execution metadata and output hashing
  • MCP schema generation from manifests

Step 1: Add ToolClad to Your Project

Add the toolclad crate to your Cargo.toml:

[dependencies]
toolclad = "0.5"

Step 2: Create a Manifest

A .clad.toml manifest defines everything about a tool: its binary, typed arguments, command template, output format, and risk tier. Save this as whois_lookup.clad.toml:

[tool]
name = "whois_lookup"
version = "1.0.0"
binary = "whois"
description = "WHOIS domain/IP registration lookup"
timeout_seconds = 30
risk_tier = "low"

[tool.cedar]
resource = "PenTest::ScanTarget"
action = "execute_tool"

[args.target]
position = 1
required = true
type = "scope_target"
description = "Domain name or IP address to query"

[command]
template = "whois {target}"

[output]
format = "text"
envelope = true

[output.schema]
type = "object"

[output.schema.properties.raw_output]
type = "string"
description = "Raw WHOIS registration data"

The scope_target type automatically validates that the input is a safe hostname, IP address, or CIDR — no wildcards, no shell metacharacters, no traversal sequences.

Step 3: Load and Validate a Manifest

Load a manifest from disk and inspect its contents. ToolClad validates the manifest structure on load — missing templates, invalid risk tiers, and enum args without allowed lists are all caught immediately.

use toolclad::load_manifest;

let manifest = toolclad::load_manifest("tools/whois_lookup.clad.toml").unwrap();

println!("Tool: {} v{}", manifest.tool.name, manifest.tool.version);
println!("Binary: {}", manifest.tool.binary);
println!("Risk tier: {}", manifest.tool.risk_tier);
println!("Timeout: {}s", manifest.tool.timeout_seconds);

// Inspect arguments
for (name, arg) in &manifest.args {
    println!("  arg '{}': type={}, required={}", name, arg.type_name, arg.required);
}

You can also validate individual arguments against their type definitions:

use toolclad::validator::validate_arg;

// Valid: a clean domain name
let result = validate_arg("target", &manifest.args["target"], "example.com");
assert!(result.is_ok());

// Rejected: shell metacharacters
let result = validate_arg("target", &manifest.args["target"], "example.com; rm -rf /");
assert!(result.is_err());

Step 4: Execute a Tool with Validated Arguments

Execute a tool by passing a map of argument values. ToolClad validates every argument, renders the command template, runs the binary with the configured timeout, and wraps the result in an evidence envelope.

use std::collections::HashMap;
use toolclad::executor::execute;

let manifest = toolclad::load_manifest("tools/whois_lookup.clad.toml").unwrap();

let mut args = HashMap::new();
args.insert("target".to_string(), "example.com".to_string());

let envelope = execute(&manifest, &args).unwrap();
println!("{}", serde_json::to_string_pretty(&envelope).unwrap());

The executor constructs the command whois example.com from the template, runs it with a 30-second timeout, and returns a structured envelope.

Step 5: Generate MCP Schema from Manifest

Generate a Model Context Protocol-compatible JSON schema directly from a manifest. This bridges ToolClad manifests to any MCP-compatible agent runtime.

use toolclad::{load_manifest, generate_mcp_schema};

let manifest = load_manifest("tools/whois_lookup.clad.toml").unwrap();
let schema = generate_mcp_schema(&manifest);
println!("{}", serde_json::to_string_pretty(&schema).unwrap());

The generated schema includes inputSchema with typed properties and required fields, plus an outputSchema that wraps your declared results in the evidence envelope format when envelope = true.

// Output:
{
  "name": "whois_lookup",
  "description": "WHOIS domain/IP registration lookup",
  "inputSchema": {
    "type": "object",
    "properties": {
      "target": {
        "type": "string",
        "description": "Domain name or IP address to query"
      }
    },
    "required": ["target"]
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "status": { "type": "string", "enum": ["success", "error"] },
      "scan_id": { "type": "string" },
      "tool": { "type": "string" },
      "command": { "type": "string" },
      "duration_ms": { "type": "integer" },
      "timestamp": { "type": "string", "format": "date-time" },
      "output_file": { "type": "string" },
      "output_hash": { "type": "string" },
      "results": { ... }
    }
  }
}

Step 6: Use Evidence Envelopes

When envelope = true in the manifest, every execution result is wrapped in an evidence envelope that provides a tamper-evident audit trail:

// Evidence envelope returned by execute()
{
  "status": "success",
  "scan_id": "a3f1c9e2-7b4d-4e8a-9c6f-1d2e3f4a5b6c",
  "tool": "whois_lookup",
  "command": "whois example.com",
  "duration_ms": 1842,
  "timestamp": "2026-03-22T10:15:30Z",
  "output_file": "/tmp/toolclad/whois_lookup_a3f1c9e2.txt",
  "output_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb924...",
  "results": {
    "raw_output": "Domain Name: EXAMPLE.COM\nRegistrar: ..."
  }
}

Key fields in the envelope:

  • scan_id — Unique identifier for this execution
  • command — The exact command that was run (for audit)
  • duration_ms — Wall-clock execution time
  • output_hash — SHA-256 hash of the raw output for tamper detection
  • output_file — Path to the raw output on disk

The envelope ensures that downstream consumers (other agents, logging systems, compliance tools) can verify that tool output has not been modified after execution.