Add a tool
Declare a tool in the manifest, implement it on the Host, keep the allow-list signed.
GemmaPod tools follow a strict pattern:
- Declared in the signed manifest. Pods can never grow new tools at runtime.
- Implemented in the Host's local registry. The pod doesn't carry executable tool code.
- The Host enforces the intersection. A tool runs only if its name is in both the signed manifest and the local registry.
This is the trust boundary. Tampering with either side breaks the contract closed.
Step 1. Declare in pod.toml
name = "ticketing-bot"
persona = "Helps customers diagnose issues and open tickets."
model = "gemma4:e4b"
system_prompt = """
You help customers. When they describe a problem you can't resolve in
chat, call `open_ticket` with a summary; never invent ticket IDs.
"""
[transport]
preferred = ["dartc", "fallback"]
[transport.dartc]
signal_url = "wss://signal.gemmapod.com/signal"
pod_id = "ticketing-bot"
[[tools]]
name = "open_ticket"
description = "Open a ticket in the company's helpdesk."
[[tools]]
name = "search_kb"
description = "Search the public knowledge base."Rebuild:
gemmapod build pod.toml --key owner.key --out ticketing-bot.htmlThe manifest's tool allow-list is now part of the signed CBOR. A tampered blob fails verification.
Step 2. Implement on the Host
The Host's local registry is keyed by tool name. Today the registry
ships three built-in tools (share_contact, show_project,
package_demo_pod); you can extend it by editing
packages/host/src/toolRuntime.ts
or — for plug-in style — wrapping the Host with your own runner.
A minimal local registry entry looks like:
const localTools: Record<string, LocalTool> = {
open_ticket: {
description: "Open a ticket in the helpdesk.",
async execute({ summary, severity = "normal" }) {
const res = await fetch("https://helpdesk.internal/api/tickets", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.HELPDESK_TOKEN}`,
},
body: JSON.stringify({ summary, severity }),
});
if (!res.ok) throw new Error(`helpdesk: ${res.status}`);
return { id: (await res.json()).id };
},
},
search_kb: {
description: "Search the public knowledge base.",
async execute({ query, limit = 5 }) {
const res = await fetch(
`https://help.example.com/api/search?q=${encodeURIComponent(query)}&limit=${limit}`,
);
return await res.json();
},
},
};A future release will expose this as a registration API
(host.registerTool(name, fn)) so you don't fork the package. For
v0.1, fork or wrap.
UI tools are separate from manifest tools
Manifest tools (like open_ticket above) are signed — they must be
declared in pod.toml and verified at runtime. UI tools are not signed;
they are registered by the Host at agent creation time and drive the visitor's
UI through DARTC events.
The Host ships two categories of UI tools:
| Category | Tools | Registration |
|---|---|---|
| Generic | show_presentation, set_state, send_custom_event | Auto-registered when sendUiEvent is provided |
| Companion-specific | react_companion, say_companion | Opt-in via uiTools: buildCompanionTools(...) |
If you're building a host embed without a 3D avatar, you don't need
companion tools. If you're building a custom dashboard, you can add
your own UI tools that emit CUSTOM events your embed understands.
See UI tools and companion plugins for the full pattern.
Step 3. Run with OWNER_PUBKEY set (production)
In production, set OWNER_PUBKEY in the pod's pod.toml so the Host
only accepts manifests signed by your key. Otherwise anyone running
a pod against your pod_id could request your tools.
gemmapod run ./ticketing-botHow the call flows
- The pod's WebRTC channel opens; both sides exchange signed
dartc.helloenvelopes carrying the signed manifest. - The Host verifies the manifest, checks
OWNER_PUBKEYif set, intersects the manifest's[[tools]]list with the local registry. - The model decides to call
open_ticket. The Host invokeslocalTools.open_ticket.execute(args)and streams the result back as a signedgemmapod.ui.event(TOOL_CALL_START→TOOL_CALL_ARGS→TOOL_CALL_END→TOOL_CALL_RESULT) plus an assistantTEXT_MESSAGE_*reply. - The browser runtime fires
ui.eventon its bus. Custom UIs render tool activity inline (see restaurant-pod).
See also
pod.tomlreference- Signed manifest reference
- Security model — tool-execution trust boundary
- Host README