Tallgeese AI Developers
Lesson 5 of 8⏱ 10 minSome code

Build a tool

Write a small sandboxed program that gives Tallgeese a new ability.

In 30 seconds

  • A tool is a small Rust program that runs safely in a sandbox.
  • It can only reach the web addresses you list.
  • Keep descriptions and answers short, and make errors helpful.

Build a tool

When there's no service to connect to, or you want something to run on the person's computer, you can write a tool. A tool is a small program, written in Rust and compiled to WebAssembly, that runs inside a sandbox. The sandbox is what makes it safe to offer to strangers: your tool can only reach what you declare.

Your tool canYour tool can't
  • Call the web addresses you list
  • Keep its own small database (if you ask)
  • Read the settings people give it, even secret keys
  • Know today's date for the person
  • Read or write files
  • Reach any address you didn't list
  • See other items or their data
  • Run for more than 30 seconds per call

The smallest tool

A tool answers two questions: "what can you do?" and "do this". Here's the shape, reduced from a real item that talks to a reference manager on the person's computer:

wit_bindgen::generate!({ path: "wit", world: "tool" });

use exports::tallgeese::tool::tools::Guest;
use serde_json::{json, Value};
use wstd::http::{Body, Client, Request};

struct Tool;

impl Guest for Tool {
    // "What can you do?": a JSON list of tools, each with a name and a description
    fn describe() -> String {
        json!([{
            "name": "list_collections",
            "description": "List the collections in the person's library, with their keys.",
            "inputSchema": { "type": "object", "properties": {}, "required": [] }
        }])
        .to_string()
    }

    // "Do this": run the named tool and answer in plain text
    fn call(name: String, arguments: String) -> Result<String, String> {
        let _args: Value = serde_json::from_str(&arguments).map_err(|_| "The arguments aren't JSON.".to_string())?;
        wstd::runtime::block_on(async move {
            match name.as_str() {
                "list_collections" => collections().await,
                other => Err(format!("There is no tool named {other}.")),
            }
        })
    }
}

export!(Tool);

async fn collections() -> Result<String, String> {
    let request = Request::get("http://127.0.0.1:23119/api/users/0/collections")
        .body(Body::empty())
        .map_err(|e| e.to_string())?;
    let response = Client::new().send(request).await
        .map_err(|_| "The app didn't answer. Ask the person to open it.".to_string())?;
    Ok(response.into_body().str_contents().await.map_err(|e| e.to_string())?.to_string())
}

Tell Tallgeese what it may reach

network = "offline"      # this tool only talks to the same computer

[[provides]]
kind = "sandboxedServer"
server = "library"
allow = { http = ["http://127.0.0.1:23119"] }
tools = { ask = ["add_reference"] }     # this one asks first, every time

Build it

rustup target add wasm32-wasip2
tgs-market build-tool items/library        # makes tool/server.wasm
tgs-market check-tools .                   # builds again and confirms it matches

Tools are built one standard way so anyone can rebuild yours and get the same file. That lets reviewers read your code and trust the result. Keep the built server.wasm in your folder.

Making tools that work well