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 can | Your tool can't |
|---|---|
|
|
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
- Web addresses are written exactly:
https://host:443(with the port), orhttp://127.0.0.1:PORTfor an app on the same computer. Plainhttpis only allowed for the same computer. - If your tool uses the internet, set
network = "online". - Put anything that changes something in
ask.
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
- Keep descriptions short. They're shown to the assistant every time, and have a size limit (8 KB).
- Answer in plain text, not raw data, and keep each answer bounded. If there's more, end with "More: start=25" so the assistant can ask for the next part.
- Make errors helpful. "Zotero didn't answer. Ask the person to open Zotero and try again" beats "connection failed".
- Check what goes into addresses. If a tool takes an id and puts it in a web address, reject anything that doesn't look like an id.
- Answers longer than 20,000 bytes are cut, with a note saying so.