SDK & API reference
Reference
Every file, field, interface and limit, in one place. For the story of how these fit together, read the developer guide.
Fields marked required must be present. Fields marked optional may be left out. Anything marked "checked" is enforced by tgs-market check, so a clean check means a release will accept it.
Versions
| Surface | Current | Compatibility |
|---|---|---|
| Catalog format | schema = 1 | The catalog's version only goes up. The app keeps no catalog older than the one it has. |
| Tool interface | tallgeese:tool@0.2.0 | Tools built for 0.1.0 keep working unchanged. A tool says which version it was built for by what it exports. |
| Page host API | apiVersion = 1 | host.init() throws if the app speaks a different version. |
| Manifests | schema = 1 | A manifest for a newer schema is refused as "for a newer app". |
An older app shows an item that uses a part or grant it doesn't know as needs a newer app, and never installs half of it.
Repository and market.toml
market.toml
items/<id>/
item.toml
text/{en,zh-Hans,zh-Hant,ja,ko}.toml
logo.png | logo.svg (optional)
skills/<name>/SKILL.md [+ files] (skillPack)
tool/{Cargo.toml, Cargo.lock, src/, wit/, server.wasm} (sandboxedServer)
agent/agent.toml, agent/skills/<name>/ (agent)
plugin.toml (agent)
page/{package.json, package-lock.json, src/, …} (page; dist/ is built)
market.toml
publisher = "Tallgeese" # default publisher where an item.toml has none
[[categories]] # categories beyond the app's own
id = "travel"
text = { en = "Plan a trip", zh-Hans = "规划旅行", zh-Hant = "規劃旅行", ja = "旅行を計画する", ko = "여행 계획하기" }
| Field | Rules (checked) |
|---|---|
publisher | Who an item is by, where its own item.toml doesn't say. Every item needs one, here or in its own file. |
[[categories]].id | 2–32 lowercase letters, digits and hyphens. Unique. May not restate an app category: read, write, plan, design, notes, web. |
[[categories]].text | A non-blank name in each of the five languages, and no other language. |
Item ids document-reader and web-search are the app's own and are reserved: a catalog can't restate them or use them as ids.
item.toml
Top-level fields
| Field | Type | Rules (checked) |
|---|---|---|
version required | string | 1–32 characters: letters, digits, dots and hyphens, starting with a letter or digit. Raise it for any change to any part. |
publisher optional | string | Overrides market.toml. Not blank. |
categories required | list of strings | At least one. Each is an app category or one defined in market.toml. |
network required | "offline" | "online" | Must be "online" if anything reaches the internet: a remoteServer, or a sandboxedServer with an https origin. |
icon required | table | glyph and tint from the lists below; optional logo. |
platforms optional | list | Computers it is for; empty means all. Values: windows-x86_64, macos-aarch64, linux-x86_64, linux-aarch64. |
[offer] required | table | kind = "free", or kind = "paid" (see below). |
[[provides]] required | array of tables | One block per part. At least one. See the parts below. |
icon
| Field | Values |
|---|---|
glyph | document, notebook, palette, checklist, folder, writing, notes, globe, chart |
tint | sand, sage, sky, plum, teal, slate |
logo optional | A file beside item.toml: PNG or SVG, at most 24 KB, PNG at most 1024 px on a side (128 px is plenty). Carried inside the signed catalog; shown on a white tile. The glyph and tint stand in where it can't be shown. |
offer
[offer]
kind = "free"
[offer]
kind = "paid"
price = "US$4.99" # shown on the page; at most 24 characters
amount = 499 # charged, in the currency's smallest unit; more than 0
currency = "usd" # three lowercase letters
tax_code = "txcd_10202000" # optional, one of Stripe's
A paid item must bring skills (a skillPack): what is paid for is the download. The checkout charges the amount and currency from the signed catalog and nowhere else.
Part: skillPack
[[provides]]
kind = "skillPack"
skills = ["meeting-minutes", "follow-up-email"]
| Field | Rules (checked) |
|---|---|
skills | Non-empty. Each name has a folder skills/<name>/ and each folder is named here. Names are up to 64 lowercase letters, digits and hyphens. A skill name is unique across the whole catalog, whichever item brought it. |
At most one skillPack per item. All of the item's skills go in it. A pack is at most 1 MB. Delivered as <item>-<version>.tgspack.
Part: remoteServer
[[provides]]
kind = "remoteServer"
server = "sample-wiki"
url = "https://wiki.example/mcp"
auth = "oauth"
tools = { only = ["search", "read_page", "write_page"], ask = ["write_page"] }
| Field | Rules (checked) |
|---|---|
server | Name among the item's parts: up to 32 lowercase letters, digits and hyphens. The app keeps the server under the item's id (tg-<item>). |
url | https, with a host, and no user@ sign-in details, no whitespace or control characters. |
auth | "oauth": the person signs in in their browser. "none": no sign-in. |
tools.only | Tools offered to the agent. Empty or absent means all. Names are 1–64 characters of letters, digits, _, . and -, each once. |
tools.ask | Tools that ask before every use. Each name must also be in only when only is set. |
Requires network = "online". One server (remote or sandboxed) per item. Nothing is downloaded: the item holds only the address.
How sign-in works
The first time the person presses Sign in, the app registers itself with the service (RFC 7591, dynamic client registration) under the name Tallgeese, then hands the result to the agent, which completes the browser sign-in. Services that don't accept unknown apps, or that require a client secret, can't be used yet.
Part: sandboxedServer
[[provides]]
kind = "sandboxedServer"
server = "finance"
grants = ["storage"]
allow = { http = ["https://query1.finance.yahoo.com:443", "https://www.sec.gov:443"] }
tools = { ask = ["record_trade"] }
| Field | Rules (checked) |
|---|---|
server | As for remoteServer. |
grants optional | What else of the computer the tool is given. Only "storage" (a database of its own). Each once. An app from before a grant offers the item as "for a newer app" rather than run the tool without it. |
allow.http optional | The origins the tool may reach, each once and written exactly: https://<host>:<port> with the port written out (443), lowercase, a DNS name with at least two labels; or http://127.0.0.1:<port> / http://[::1]:<port> for this computer. Never localhost, a private address, or http to a named host. |
tools.only, tools.ask | As for remoteServer. Names must be tools the component really describes. |
Needs tool/ with the source and the built server.wasm (at most 4 MB). An https origin requires network = "online". Delivered as <item>-<version>.tgstool.
Part: runtime
[[provides]]
kind = "runtime"
family = "python"
family is one of the app's own add-ons: today "python". The catalog names a family and never an address or hash; the app downloads its own pinned, hash-checked build the first time an item that asks for one is got, and removes it with the last item that uses it. An item with a runtime must also bring a skillPack or a server that uses it. At most one.
Part: agent
[[provides]]
kind = "agent"
role = "tg_financial_advisor"
area = "their investments, portfolio or watch list, or prices or news about a stock or fund"
| Field | Rules (checked) |
|---|---|
role | Must be tg_ plus the item id with hyphens as underscores (financial-advisor → tg_financial_advisor). It must equal name in agent/agent.toml. |
area | What the main chat asks the specialist about, up to 300 characters. Written for the assistant: it becomes the description of the ask_<role> tool. |
Needs a sandboxedServer, agent/agent.toml, and (if it has any) agent/skills/. At most one. Delivered in <item>-<version>.tgsplugin with the page.
Part: page
[[provides]]
kind = "page"
id = "financial-advisor"
glyph = "chart"
calls = ["list_portfolio", "asset_mix", "delete_account"]
confirms = ["delete_account"]
actions = ["trade", "review"]
| Field | Rules (checked) |
|---|---|
id | The item's id. |
glyph | One of the glyphs listed under icon. Drawn in the sidebar. |
calls | Tools the page may call itself. Each must be a tool the item's tool really offers. Each once. |
confirms | Tools in calls that ask first (in tools.ask, or in a [[permissions]] whose default is ask): the page asks the person itself before calling. Every tool in calls that asks must be in confirms, and every tool in confirms must be in calls and must ask. |
actions | Up to six ids (lowercase letter then letters, digits, _; up to 32). Buttons in the page header that start a chat message. Each needs text under [page] "actions.<id>" in all five languages. |
Needs an agent. At most one page. The page's files are in the same .tgsplugin as the agent.
text/<locale>.toml
name = "Financial Advisor"
tagline = "Follow your investments, with prices and news"
description = "A watch list of the stocks and funds you follow…"
tasks = ["Keep an eye on stocks and funds", "See how my investments are doing"]
prompts = ["Watch Tesla and Amazon for me", "How did my investments do this month?"]
[plugin] # specialists: labels for settings, groups and permissions
"settings.price_service" = "Price service"
"settings.price_service.tiingo" = "Tiingo (US stocks and funds)"
"groups.prices" = "Get prices instantly with your own free key"
"permissions.trades" = "Record a trade"
[page] # specialists: action buttons, and the page's own words
"actions.trade" = "I'd like to record a trade I made"
"advisor.disclaimer" = "It isn't professional financial advice."
| Field | Rules (checked) |
|---|---|
| Files | Exactly one per language: en, zh-Hans, zh-Hant, ja, ko. No other file names. |
name | Not blank, at most 60 characters. |
tagline | Not blank, at most 160 characters. |
description | Not blank, at most 2,000 characters. |
tasks, prompts | One to six each, every line at most 160 characters. The count in every language equals the English count. |
[plugin] | Exactly the keys the item needs, no others: settings.<id>, settings.<id>.<choice> for each choice, groups.<group>, permissions.<id>. Each a line of at most 160 characters. An item without an agent has neither table. |
[page] | actions.<id> for every action (up to 160 characters), plus the page's own strings (what host.init().strings returns): keys of letters, digits, _ and . up to 80 characters, text up to 600 characters, the same keys in every language. |
SKILL.md
---
name: meeting-minutes
description: "Write meeting minutes from rough notes. Use when someone gives notes or a transcript of a meeting and wants minutes, action items, or a follow-up."
---
# Writing minutes
…
| Rule | Value (checked) |
|---|---|
| Folder name = skill name | Up to 64 lowercase letters, digits and hyphens. name in the front matter must equal it. |
| Front matter | Flat key: value lines between --- lines. Block scalars (>, |) are read. No nested blocks. Quote a value that YAML would read as something else (true, no, numbers, null). |
description | Says when to use the skill. At most 1,024 characters, but the assistant is shown only the first 200, so put the trigger first. |
Size of SKILL.md | At most 20,000 bytes. Past that, the assistant gets it with the middle cut out. |
| Extra files | .md .txt .json .csv .tsv .yaml .yml, UTF-8 text, nothing that starts like a script. File names: letters, digits, . - _. No links. |
| Whole pack | At most 1 MB. |
How the assistant reaches your skill
Tallgeese gives the assistant two read-only abilities for skills:
| Tool | Does |
|---|---|
load_skill {skill, cursor?} | Its description lists up to 15 enabled skills, each as name: first 200 characters of description. Calling it with a name returns that skill's SKILL.md, 20,000 bytes at a time, with a cursor to continue. |
read_skill_file {skill, path, cursor?} | Returns a file the skill's instructions name, such as references/charts.md. Only offered if some enabled skill has extra files. |
Tool interface (WIT)
A sandboxed tool is a WebAssembly component built for wasm32-wasip2 that exports one small interface. The app serves it to the agent as an MCP server over stdio, run from the app's own program.
Version 0.1.0
package tallgeese:tool@0.1.0;
interface tools {
/// Its tools, as a JSON array of MCP tool descriptions:
/// [{ "name", "description", "inputSchema" }, ...]. The same every time.
describe: func() -> string;
/// Runs the tool `name`. `arguments` is the JSON object the agent sent.
/// ok: the answer, as text for the agent; err: the same, as an error.
call: func(name: string, arguments: string) -> result<string, string>;
}
world tool { export tools; }
Gets HTTP (through wasi:http) to the origins in allow.http, and nothing else.
Version 0.2.0
package tallgeese:tool@0.2.0;
interface types {
variant value { null, integer(s64), real(f64), text(string), blob(list<u8>) }
type row = list<value>;
record statement { sql: string, params: list<value> }
/// `text` for the agent; `structured`, JSON, for the item's page only.
record answer { text: string, structured: option<string> }
}
/// Only for a tool whose manifest has the `storage` grant: any other that
/// imports it doesn't start. `params` are bound in order.
interface storage {
use types.{value, row, statement};
query: func(sql: string, params: list<value>) -> result<list<row>, string>;
execute: func(sql: string, params: list<value>) -> result<u64, string>;
batch: func(statements: list<statement>) -> result<_, string>;
}
/// The person's settings for the item, by id; a secret only to the tool.
interface config { get: func(id: string) -> option<string>; }
interface clock {
/// The date (YYYY-MM-DD) at `ms` since 1970 UTC, where the person is.
local-date: func(ms: s64) -> string;
}
interface tools {
use types.{answer};
describe: func() -> string;
call: func(name: string, arguments: string) -> result<answer, answer>;
}
world tool {
import storage;
import config;
import clock;
export tools;
}
| Addition | Why you'd use it |
|---|---|
storage | A SQLite database of the item's own, kept across updates. Read and written by both the agent's server and the item's page. |
config | Read the person's settings, including secret settings, which are stored safely and never shown. A choice or text setting is its value; unset is none. |
clock.local-date | WebAssembly has no time zone. Pass ms since 1970 and get the date where the person is. |
answer.structured | JSON for the page: richer data than the text the agent reads. Up to 1 MiB. The agent never sees it. |
Sandbox and budget
Every call gets a fresh instance with WASI granted nothing, HTTP through a gate, and these limits:
| Limit | Value |
|---|---|
| Memory (all of the tool's together) | 64 MB |
| Time per call | 30 seconds |
| HTTP requests per call (allowed or not) | 32 |
| Answer to the agent | 20,000 bytes. Longer is cut at a character boundary and says so. Design your own paging ("More: start=25"). |
| Answer to the page | 1 MiB |
| Instances / tables / memories per call | 100 / 100 / 16 |
describe output | 8 KB (32 KB with an agent part). The host itself refuses over 32 KB. |
Component (server.wasm) | 4 MB |
HTTP is checked against the manifest's allow.http origin by origin, and the origin of each request is the same form as in the allow list (scheme://host:port, lowercase, port written out). A request to anything else fails.
When tools.only is set, only those tools are offered to the agent; the others are hidden even though the component describes them.
Storage
One SQLite file per item at app/plugins/<item>/data.sqlite, in WAL mode with a 5-second busy timeout. The agent's server and the page each hold a connection of their own; one can write while the other reads.
| Rule | Detail |
|---|---|
query | Only statements that read. Returns at most 100,000 rows: use LIMIT. |
execute | One statement, in a transaction of its own; returns rows changed. |
batch | Several statements in one transaction: all or none. |
| Refused | ATTACH; extensions; PRAGMA other than user_version and table_info; transactions of your own; any name starting _tg_ (the host's). |
| Size | 200 MB. |
| Deadline | A call's statements stop at the call's 30 seconds. |
| Change signal | Every write bumps a counter in the same transaction. The app watches it about once a second and tells the page (onChanged), whoever wrote. |
| Migrations | Yours. Use PRAGMA user_version to version your schema; upgrade your own tables the first time the tool opens at a new version. Data lives outside any version, so updates keep it. |
Project and build
[package]
name = "zotero-tool"
version = "1.0.0"
edition = "2021"
license = "MIT"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = "0.63"
wstd = "0.6" # async HTTP and timers on wasi:http
serde_json = "1"
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
strip = true
panic = "abort"
# Built on its own, for WebAssembly.
[workspace]
wit_bindgen::generate!({ path: "wit", world: "tool" });
use exports::tallgeese::tool::tools::{Answer, Guest};
use tallgeese::tool::types::Value;
use tallgeese::tool::{clock, config, storage};
struct Tool;
impl Guest for Tool {
fn describe() -> String { /* JSON array of tools */ String::new() }
fn call(name: String, arguments: String) -> Result<Answer, Answer> {
let key = config::get("price_key"); // Option<String>
let today = clock::local_date(1_700_000_000_000); // "YYYY-MM-DD"
let rows = storage::query("SELECT n FROM t WHERE id = ?1", &[Value::Integer(1)]);
Ok(Answer { text: "…".into(), structured: Some("{\"n\":1}".into()) })
}
}
export!(Tool);
Put the matching tool.wit (0.1.0 or 0.2.0, both shown above) in tool/wit/. The builder uses a fixed Rust toolchain supplied with the Developer Kit:
rustup target add wasm32-wasip2
tgs-market build-tool items/<id> # cargo build --release --locked --target wasm32-wasip2
The build records no paths from your computer, so the same source gives the same file everywhere. tgs-market check-tools builds again and fails if the result differs from the server.wasm you saved.
The tool's signed manifest tool.json
You don't write it: the release does. For reference, it is a signed envelope holding one object under tool:
{ "tool": {
"schema": 1, "item": "zotero", "version": "1", "server": "zotero",
"component": { "path": "server.wasm", "size": 123456, "digest": "sha256:…" },
"allow": { "http": ["http://127.0.0.1:23119"] },
"tools": { "only": [], "ask": ["add_reference"] },
"grants": ["storage"] // omitted when none
} }
The app verifies the signature, the item and version, and the component's size and digest every time the tool starts, and refuses a tool whose manifest isn't the one the person approved (a fingerprint of the signed payload).
agent/agent.toml
name = "tg_financial_advisor"
description = "The person's investments: …"
model_reasoning_effort = "medium"
nickname_candidates = ["Advisor"]
developer_instructions = """
You are the Financial Advisor, a specialist inside Tallgeese. …
"""
| Key | Rules (checked) |
|---|---|
name required | Equals role in item.toml. |
description required | What the specialist is. |
developer_instructions required | At most 16 KB. They go to the assistant with every request in the specialist's threads. |
model_reasoning_effort optional | minimal, low, medium, high or xhigh. |
nickname_candidates optional | Optional display names for the specialist. |
Nothing else is allowed. Keys that would change tools, the sandbox, approvals or the assistant (such as mcp_servers, sandbox_mode, approval_policy or model) are refused: the app controls those.
What the app adds to the specialist's instructions
- Your
developer_instructions. - "The person's settings": for example the currency, and whether a price service is set. Never a secret.
- "Your skills": each of
agent/skills/*by name, description and file, to read when one applies.
The app assembles them each time it starts a specialist thread, so a changed setting applies to the next one. The thread has your tool switched on; the main conversation's never does. agent/ holds agent.toml and skills/ and nothing else. Skills in agent/skills/ follow the same rules and may not share a name with the item's pack skills.
The main chat's ask_<role> tool
The app adds one tool per enabled specialist to every main conversation (and every scheduled run). Its description is your area, with "it holds the person's data and the tools for it; you don't", and instructs the assistant to pass the request as the person put it and relay the answer with its caveats. When called, the app runs a turn on the specialist's thread and answers with its last message. A failed turn answers as an error that says why. The exchange shows in the main chat as an "Asked <name>" card.
plugin.toml
Settings the person can change, permissions they can set, and what the tool keeps. Only for items with an agent.
[[settings]]
[[settings]]
id = "home_market"
kind = "choice"
choices = ["auto", "US", "UK", "JP"]
default = "auto"
group = "market"
[[settings]]
id = "price_key"
kind = "secret"
group = "prices"
links = { tiingo = "https://www.tiingo.com/account/api/token" }
| Field | Rules (checked) |
|---|---|
id | Lowercase letter, then lowercase letters, digits, _; up to 32. Unique. This is what config::get takes. |
kind | choice, secret (stored safely, never in a plain file), or text. |
choices | Only for choice: two or more, each once, each up to 32 characters of letters, digits, _, -. |
default | For choice/text: must be one of choices for a choice. A secret has no default. |
group | Optional. Settings with the same group are shown together under groups.<group>. Lowercase letters, digits, _; up to 32. host.openSettings("prices") opens at a group. |
links | Optional. Where the person gets what the setting asks for, keyed by a choice of some setting. Values are https addresses. |
[[permissions]]
[[permissions]]
id = "trades"
default = "ask" # or "allow"
tools = ["record_trade"]
A permission the person can switch between "ask first" and "allow". tools are tools the item's tool really offers; a tool is in at most one permission. A permission's id is lowercase letters, digits, _; up to 32; unique.
[data]
[data]
storage = true # must go with grants = ["storage"] in item.toml
export = ["json", "csv"] # the formats "Export my data" offers
Text keys the checker expects
For each setting, "settings.<id>"; for each choice, "settings.<id>.<choice>"; for each group, "groups.<group>"; for each permission, "permissions.<id>" — all under [plugin] in every language's text/*.toml.
Page API (host)
A page is a web page that runs in a sandboxed frame: sandbox="allow-scripts", no origin of its own, no network. The frame is served as tgsplugin://localhost/<item>/<version>/index.html (http://tgsplugin.localhost/… on Windows). The app hands the page one MessagePort when it loads, and answers only on that port. @tallgeese/plugin-page wraps it as host.
import { host } from "@tallgeese/plugin-page";
const told = await host.init(); // call this first; anything before it is refused
Content Security Policy
default-src 'none';
script-src 'unsafe-inline' tgsplugin: http://tgsplugin.localhost;
style-src 'unsafe-inline' tgsplugin: http://tgsplugin.localhost;
img-src data: tgsplugin: http://tgsplugin.localhost;
font-src data: tgsplugin: http://tgsplugin.localhost;
base-uri 'none'; form-action 'none'
There is no connect-src: a page can't fetch. Inline script is allowed because a page is usually one file (Vite's single-file build). The package serves only files its signed manifest lists, and checks each one's digest as it serves it.
Methods
| Call | Returns | What it does |
|---|---|---|
host.init() | Init | Starts the session. See Init below. |
host.call(tool, args?) | { text, structured, isError } | Runs one of the item's tools listed in calls, in the app's process, against the same storage as the agent's. The answer is the tool's result as MCP gives it: structured is the answer.structured JSON string parsed. A call that partly succeeded is an error with what it found. |
host.ask(text, context?) | void | Opens the chat card and sends text as the person, with context as a line before it ({ symbol: "VTI" }). Only in answer to the person's click; at most 4,000 characters. With empty text and a context, opens the chat for the person to write about it. |
host.openSettings(group?) | void | Opens the item's settings in the app, at a group. This is where secrets are typed, never in the page. |
host.openLink(url) | void | Opens an https address in the person's browser. Only in answer to a click. |
host.keep(key, value | null) | void | Keeps a string for the page on this computer (the frame has no storage). null forgets. init().kept hands it back. |
host.shareBar() | Bar | Share the app's header row: the app draws the item's name and its buttons over the page, and the page puts controls in the space between. Returns { height, start, end } in CSS pixels. |
Init
interface Init {
apiVersion: number; // 1
item: string;
version: string;
locale: string; // the person's language
strings: Record<string, string>; // your [page] text in that language (or English)
theme: { mode: "light" | "dark"; tokens: Record<string, string> };
settings: Record<string, unknown>; // what the page may see; a secret only as true/false
calls: string[]; // tools the page may call
confirms: string[]; // of those, ask the person yourself first
kept: Record<string, string>; // what host.keep stored
}
Events
| Subscribe | Fires when |
|---|---|
host.onChanged(f) | The tool's storage changes, whoever changed it. Returns an unsubscribe function. |
host.onTheme(f) | The person switches light/dark: f(theme). |
host.onLocale(f) | The person changes language: f(locale, strings). |
host.onChat(f) | The page's chat starts or stops answering: f({ busy }). |
host.onBar(f) | The shared header row changes (only after shareBar()). |
host.onSettings(f) | The person changes the item's settings: f(settings). |
Theme tokens
theme.tokens maps names, without the leading --, to values (primary, surface, …). Apply them as CSS custom properties so the page looks like the app:
for (const [name, value] of Object.entries(theme.tokens))
document.documentElement.style.setProperty(`--${name}`, value);
document.documentElement.dataset.theme = theme.mode;
Log the tokens once in your page's development build to see which names the app currently provides.
Rules and limits
| Limit | Value |
|---|---|
| Tool calls | At most 20 per second; only tools in calls; arguments must satisfy the tool's schema. |
ask | 3 per 10 seconds; text up to 4,000 characters; only while the person is in the page. |
openLink | 3 per 10 seconds; https only; only while the person is in the page. |
openSettings | 5 per 10 seconds. |
keep | 32 keys; keys up to 64 characters; values up to 4,096 characters. |
| Built page | dist/index.html plus .html .js .css .svg .png .webp .woff2 .json; 5 MB total; file names letters, digits, . - _; HTML, CSS and SVG may not load anything from another origin (src, href, url() to //…, @import). |
What a page never gets: the conversation, approvals, secrets, the person's files, other items' tools, or the network. A frame that loads a second time has navigated away and gets no port.
Recommended build: Vite with base: "./" and a single-file output. React works (about 45 KB gzipped); Preact is smaller. tgs-market build-page items/<id> builds it using the lock file you saved; review builds it twice and fails if the two results differ.
Built-in tools you can rely on
These ship in the app, are always available to the agent, and need no item. Your skills may name them.
Web: search and fetch
| Tool | Arguments | Returns |
|---|---|---|
search | query (string, required); max_results (1–10, default 6); when (day, week, month, year) | Titles, addresses and snippets. |
fetch | url (string, required); offset (integer) | The page's main content as Markdown, a part at a time. A PDF or Office file is saved and its path returned, to read with read_document. |
Documents: document_info, read_document, read_sheet, render_pdf_page
| Tool | Arguments | Returns |
|---|---|---|
document_info | path | Pages, sections, chapters, slides or sheets; outline; how many reads it takes. |
read_document | path; pages ("5", "3-8", "10-"); cursor | The text as Markdown, a part at a time, ending with how to continue. |
read_sheet | path; sheet; range ("A1:F200") | A Markdown table with row numbers and column letters. Values as last saved; formulas aren't recalculated. |
render_pdf_page | path; page; width (400–2000) | One PDF page as an image. |
Formats: PDF, Word (.docx, .doc), PowerPoint (.pptx, .ppt), spreadsheets (.xlsx, .xls, .ods, .csv, .tsv), OpenDocument (.odt, .odp), Hangul (.hwp, .hwpx), RTF, EPUB, and text or Markdown in any encoding.
Skills: load_skill, read_skill_file
Described above.
If a tool answers with too much, the middle is dropped without warning. So the built-in tools fill each result to a fixed budget and say where to continue. Do the same in your own tools.
Catalog and packages
You don't write these, but you may want to inspect them. A release is a folder:
catalog.json signed
packs/<item>-<version>.tgspack the skills
tools/<item>-<version>.tgstool tool.json (signed manifest) + server.wasm
plugins/<item>-<version>.tgsplugin plugin.json (signed manifest) + agent.toml + plugin.toml + text.json + skills/ + page/
Signed envelopes
Every document in a release is signed, and each kind is wrapped so it can't be mistaken for another.
Catalog shape
{
"schema": 1,
"version": 17, // release number; only goes up
"categories": [{ "id": "finance", "text": { "en": "…", "ja": "…" } }],
"items": [{
"id": "financial-advisor", "version": "2", "publisher": "Tallgeese",
"categories": ["finance"],
"icon": { "glyph": "chart", "tint": "sage" },
"offer": { "kind": "free" },
"network": "online",
"platforms": [], // omitted when for all
"provides": [
{ "kind": "sandboxedServer", "server": "finance",
"package": { "url": "/v1/tools/financial-advisor/2", "size": 0, "sha256": "…" },
"allow": { "http": ["https://www.sec.gov:443"], "storage": true },
"tools": { "ask": ["record_trade"] } },
{ "kind": "skillPack", "package": { "url": "/v1/packs/…", "size": 0, "sha256": "…" }, "skills": ["…"] },
{ "kind": "agent", "role": "tg_financial_advisor", "area": "…", "package": { "url": "/v1/plugins/…", "size": 0, "sha256": "…" } },
{ "kind": "page", "id": "financial-advisor", "glyph": "chart", "calls": ["…"], "confirms": ["…"], "actions": ["…"] }
],
"text": { "en": { "name": "…", "tagline": "…", "description": "…", "tasks": ["…"], "prompts": ["…"] } }
}]
}
Note that in the catalog the storage grant is allow.storage = true, while in item.toml it is grants = ["storage"]. The release converts one to the other. The app reads at most 4 MB of catalog, envelope and all.
What the app checks, and when
- On getting: the catalog's signature; each package's size and SHA-256 against the catalog; then the signed manifest is this item's; then the files are exactly the manifest's.
- On using: the tool at each start (signature, fingerprint approved by the person, component digest); each page file as it is served.
- On a new version: the person is asked again if what the item reaches, keeps or adds changed.
tgs-market command line
| Command | What it does |
|---|---|
check <content dir> | Says everything wrong with a content folder, each line naming where. Exits 1 if anything is. Starts each tool with its grants and an empty throwaway database to read what it describes. |
dev [--content <dir>] [--port 8787] | Serves your project to the app on this computer, refreshed whenever a file changes. Prints the two settings (TGS_ADDONS_URL and TGS_ADDONS_KEYS) to start the app with. |
build-tool <item dir> | Builds tool/server.wasm from tool/, the one way tools are built. |
build-page <item dir> | Builds an agent plugin's page from page/ (with npm and the packages its lock file names) into page/dist/. |
check-tools <content dir> | Builds every tool again; exits 1 if any isn't the server.wasm beside it. |
Environment for dev
| Variable | Set it to |
|---|---|
TGS_ADDONS_URL | The address dev prints (for example http://127.0.0.1:8787). The app's Marketplace connects there. |
TGS_ADDONS_KEYS | The public key dev prints. The app trusts releases signed with it. |
The dev daemon reads them when it starts: TGS_ADDONS_URL=… TGS_ADDONS_KEYS=… npm run dev.
All limits in one table
| What | Limit |
|---|---|
| Item id | 2–48 lowercase letters, digits, hyphens |
| Version | 1–32 letters, digits, dots, hyphens |
| Server name | Up to 32 lowercase letters, digits, hyphens |
| Tool name | 1–64 letters, digits, _, ., - |
| Skill name | Up to 64 lowercase letters, digits, hyphens |
| Name / tagline / description | 60 / 160 / 2,000 characters |
| Tasks / prompts | 1–6 each, 160 characters per line, same count in every language |
| Logo | 24 KB; PNG up to 1024 px |
| Price text | 24 characters |
| Skill description | 1,024 characters; 200 shown to the assistant |
| Skills listed to the assistant | 15 |
SKILL.md | 20,000 bytes |
| Skill pack | 1 MB |
| Tool component | 4 MB |
| Tool descriptions | 8 KB (32 KB with a specialist) |
| Tool call | 64 MB memory, 30 s, 32 HTTP requests, 20,000-byte answer to the agent, 1 MiB to the page |
| Storage | 200 MB; 100,000 rows per query |
| Specialist instructions | 16 KB |
Specialist area | 300 characters |
| Page actions | 6 |
| Built page | 5 MB |
| Page strings | 600 characters each; [plugin] and action labels 160 |
| Catalog envelope | 4 MB |
Checker messages and what to do
The checker prints every problem, each starting with where it is. Some you will meet first:
| Message (shortened) | Fix |
|---|---|
no name in ja / ko.toml isn't one of the app's languages | Add the missing text/ja.toml, or rename a stray file to one of the five locales. |
ja.toml has 2 tasks and 3 prompts; en.toml has … | Make every language list the same number of tasks and prompts. |
no category … | Use an app category, or add it to market.toml with names in five languages. |
something reached over the internet has network = "online" | Set network = "online". |
SKILL.md: its name is the folder's | Make the front-matter name equal the folder name. |
a skill holds text (…), nothing that runs | Remove the script or unsupported file; keep text. |
SKILL.md is N bytes; past 20000 the assistant gets it with the middle cut out | Move long material into references/. |
allow.http: … isn't an origin a tool may be given | Write it exactly: https://host:443 or http://127.0.0.1:PORT. |
no server.wasm: build the tool … | Run tgs-market build-tool and commit the result. |
the tools it offers are described in N bytes; at most 8192 | Shorten descriptions, or drop tools. 32 KB applies only with an agent. |
calls names X, which asks first, so confirms names it too | Add X to confirms, or take it out of calls. |
an agent's role is tg_ and the item's id with underscores | Rename the role to match the item id. |
[page] has no "…" / [plugin] has no "…" | Add the key to that language's text/*.toml. |
dist/…: … loads from another origin | Inline or bundle the asset; a page holds everything it loads. |
only an item that brings skills can be sold | Add a skillPack, or make the item free. |