Let your agents cook.
One binary. Any agent framework. Every tool your agent needs.
ATI gives AI agents secure access to APIs, MCP servers, OpenAPI services, and local CLIs — through one unified interface. No custom tool wrappers. No per-SDK plumbing. Your agent calls ati run <tool> --arg value and ATI handles auth, protocol bridging, and response formatting.
Download the latest release for your platform:
# macOS (Apple Silicon)
curl -fsSL https://github.com/Parcha-ai/ati/releases/latest/download/ati-aarch64-apple-darwin.tar.gz \
| tar xz && sudo mv ati /usr/local/bin/
# macOS (Intel)
curl -fsSL https://github.com/Parcha-ai/ati/releases/latest/download/ati-x86_64-apple-darwin.tar.gz \
| tar xz && sudo mv ati /usr/local/bin/
# Linux (x86_64, static musl binary)
curl -fsSL https://github.com/Parcha-ai/ati/releases/latest/download/ati-x86_64-unknown-linux-musl.tar.gz \
| tar xz && sudo mv ati /usr/local/bin/
# Linux (ARM64, static musl binary)
curl -fsSL https://github.com/Parcha-ai/ati/releases/latest/download/ati-aarch64-unknown-linux-musl.tar.gz \
| tar xz && sudo mv ati /usr/local/bin/# From crates.io
cargo install agent-tools-interface
# Or from git
cargo install --git https://github.com/Parcha-ai/ati.git
# Or clone and build
git clone https://github.com/Parcha-ai/ati.git && cd ati
cargo build --release
# Binary at target/release/ati# Initialize ATI (creates ~/.ati/)
ati init
# Add a free API — zero config, no API key needed
ati provider import-openapi https://clinicaltrials.gov/api/v2/openapi.json
# Try it
ati run clinicaltrials:searchStudies --query.term "cancer" --pageSize 3Finnhub publishes an OpenAPI spec with 110 endpoints — stock quotes, company financials, insider transactions, market news. One command turns it into tools:
# Import the spec — ATI auto-derives provider name, auth, endpoints
ati provider import-openapi https://finnhub.io/api/v2/spec.json
# → Saved manifest to ~/.ati/manifests/finnhub.toml
# → Imported 85 operations from "Finnhub — Real-time stock quotes..."
# Store your API key
ati key set finnhub_api_key "your-key-here"
# 85 tools, instantly available
ati tool list --provider finnhub | head -5
# ┌──────────────────────┬──────────┬────────────────────────────────┐
# │ DESCRIPTION ┆ PROVIDER ┆ TOOL │
# ╞══════════════════════╪══════════╪════════════════════════════════╡
# │ Symbol Lookup ┆ finnhub ┆ finnhub:symbol-search │
# │ Company Profile ┆ finnhub ┆ finnhub:company-profile2 │
# │ Quote ┆ finnhub ┆ finnhub:quote │
# │ Insider Transactions ┆ finnhub ┆ finnhub:insider-transactions │
# │ Basic Financials ┆ finnhub ┆ finnhub:company-basic-... │
# └──────────────────────┴──────────┴────────────────────────────────┘Every operation in the spec is now a tool. No --name, no TOML to write, no code to generate.
# Agent asks: "research Apple stock — price, insider activity, and sentiment"
ati assist finnhub "research Apple stock — price, insider activity, and sentiment"
# Here are the exact commands to research Apple (AAPL) stock:
#
# 1. Current Price
# ati run finnhub:quote --symbol AAPL
#
# 2. Insider Transactions
# ati run finnhub:insider-transactions --symbol AAPL
#
# 3. News Sentiment
# ati run finnhub:news-sentiment --symbol AAPLati assist answers like a knowledgeable colleague — which tools, what order, key params, gotchas — with commands you can run immediately.
ati run finnhub:quote --symbol AAPL
# c: 262.52 ← current price
# d: -1.23 ← change
# dp: -0.4664 ← percent change
# h: 266.15 ← day high
# l: 261.43 ← day low
# o: 264.65 ← open
# pc: 263.75 ← previous close
ati run finnhub:insider-transactions --symbol AAPL
# data: [{name: "COOK TIMOTHY D", transactionCode: "S",
# change: -59751, share: 3280295, transactionPrice: 257.57,
# filingDate: "2025-10-03"}, ...]The agent doesn't write HTTP requests. It doesn't parse JSON responses. It calls ati run and gets structured data back — real Apple stock price, real Tim Cook insider sells.
# DeepWiki is a free MCP server — no API key needed
ati provider add-mcp deepwiki --transport http \
--url "https://mcp.deepwiki.com/mcp" \
--description "AI-powered docs for any GitHub repo"
# → Saved manifest to ~/.ati/manifests/deepwiki.toml
# See the auto-discovered tools
ati tool list --provider deepwiki
# ┌─────────────────────────────────────────────────────┬──────────┬───────────────────────────────┐
# │ DESCRIPTION ┆ PROVIDER ┆ TOOL │
# ╞═════════════════════════════════════════════════════╪══════════╪═══════════════════════════════╡
# │ Get a list of documentation topics for a GitHub ... ┆ deepwiki ┆ deepwiki:read_wiki_structure │
# │ View documentation about a GitHub repository. ┆ deepwiki ┆ deepwiki:read_wiki_contents │
# │ Ask any question about a GitHub repository ... ┆ deepwiki ┆ deepwiki:ask_question │
# └─────────────────────────────────────────────────────┴──────────┴───────────────────────────────┘
# Ask a question about any repo
ati run deepwiki:ask_question \
--repoName "anthropics/claude-code" \
--question "How does tool dispatch work?"
# Tool dispatch in Claude Code involves a dynamic system that handles both
# built-in and external Model Context Protocol (MCP) tools.
#
# 1. **Tool Name Check**: The system first checks if the tool name starts
# with `mcp:`. If not, it's routed to the built-in tool pipeline.
# 2. **MCP Tool Resolution**: Server and tool names are extracted from the
# `mcp:<servername>:<toolname>` format, the server is connected, and
# the tool is invoked using a `call_tool` RPC.
# 3. **Output Handling**: Text output is returned; images are handled
# during streaming.MCP tools are namespaced as <provider>:<tool_name>. ATI handles JSON-RPC framing, session management, and auth injection.
The examples above show a human typing commands. But ATI is designed so agents do all of this themselves — init, discover, store secrets, search across providers, and execute — with zero human intervention.
The agent can import any OpenAPI spec by URL — name auto-derived — and connect to MCP servers. No human has to write config files. ATI auto-creates ~/.ati/ on first use.
# Import APIs from their specs
ati provider import-openapi https://clinicaltrials.gov/api/v2/openapi.json
ati provider import-openapi https://finnhub.io/api/v1/openapi.json
# Connect MCP servers
ati provider add-mcp deepwiki --transport http \
--url "https://mcp.deepwiki.com/mcp"
ati provider add-mcp linear --transport http \
--url "https://mcp.linear.app/mcp" \
--auth bearer --auth-key linear_api_keyati key set finnhub_api_key sk-your-key
ati key set linear_api_key lin_api_abc123
ati key list
# cerebras_api_key csk-...tj3k
# finnhub_api_key sk-...r-key
# github_token ghs-...O6RE
# linear_api_key lin-...c123Keys are masked on output. The agent never sees raw values after storing them.
This is the key part. The agent now has dozens of providers and hundreds of tools. It doesn't need to know which provider has what — it just asks.
ati assist "do we have a tool to search for stock prices?"
# Yes, we have several tools for stock prices:
#
# **For current/latest prices:**
# - `financial_datasets:getStockPriceSnapshot` — latest price snapshot
# - `finnhub:quote` — real-time quote data (US stocks)
#
# **For historical data:**
# - `financial_datasets:getStockPrices` — OHLCV data with date ranges
#
# ati run financial_datasets:getStockPriceSnapshot --ticker AAPL
# ati run finnhub:quote --symbol AAPL
# ati run financial_datasets:getStockPrices --ticker AAPL \
# --start_date 2024-12-01 --end_date 2024-12-31 --interval day
ati tool search "sanctions"
# PROVIDER TOOL DESCRIPTION
# complyadvantage ca_business_sanctions_search Search sanctions lists for businesses
# complyadvantage ca_person_sanctions_search Search sanctions lists for individualsati assist answers naturally — which tools, why, and exact commands — like asking a colleague. ati tool search is instant and offline. The agent picks the right tool and runs it — no human in the loop.
Wrap any CLI. The agent calls ati run, ATI spawns the subprocess with credentials injected. The agent never sees the raw token.
ati provider add-cli gh --command gh \
--env 'GH_TOKEN=${github_token}'
# Agent asks how to use it
ati assist gh "how do I create a pull request?"
# Use the `pr create` subcommand:
# ati run gh -- pr create --title "Fix login bug" --body "Resolves #123"
# Your branch must be pushed first. Add --draft to open as draft,
# or --base main --head feature/new-auth to target specific branches.
# Agent runs it
ati run gh -- repo view anthropics/claude-code --json name,stargazerCount
# name: claude-code
# stargazerCount: 73682Three tiers — same ati run interface, different credential exposure:
| Dev Mode | Local Mode | Proxy Mode | |
|---|---|---|---|
| Credentials | Plaintext file | Encrypted keyring | Not in sandbox |
| Key exposure | Readable on disk | mlock'd memory | Never enters sandbox |
| Setup | ati key set |
Keyring + session key | ATI_PROXY_URL |
| Use case | Local dev | Sandboxed agents | Untrusted sandboxes |
In proxy mode, the agent's sandbox has zero credentials. All calls route through ati proxy, which holds the keys, validates JWTs, and enforces per-tool scopes:
# Orchestrator issues a scoped token
ati token issue --sub agent-7 \
--scope "tool:clinicaltrials:* tool:finnhub:* help" --ttl 3600
# Agent's sandbox — only has the binary and a JWT
export ATI_PROXY_URL=http://proxy:8090
export ATI_SESSION_TOKEN=eyJhbG...
# Same commands, routed through proxy, scoped to allowed tools
ati run clinicaltrials:searchStudies --query.term "cancer" # ✓ allowed
ati run ca_person_sanctions_search --search_term "Name" # ✗ deniedWhen JWT validation is configured, ATI treats tokens strictly: invalid or missing ATI_SESSION_TOKEN values are rejected instead of being partially trusted. If JWT validation is not configured, ATI stays in unrestricted dev mode for local workflows.
Every provider type produces the same interface: ati run <tool> --arg value. The agent doesn't know or care what's behind it. ATI ships with five user-configurable provider types (OpenAPI, MCP, Skills, CLI, HTTP) plus one built-in (file_manager).
Point ATI at an OpenAPI 3.0 spec URL or file. It downloads the spec, discovers every operation, and registers each as a tool with auto-generated schemas.
# Preview what's in a spec before importing
ati provider inspect-openapi https://petstore3.swagger.io/api/v3/openapi.json
# Import it — name derived from the URL (or pass --name to override)
ati provider import-openapi https://api.example.com/openapi.json
# If the API needs auth, ATI tells you what key to set
ati key set example_api_key sk-your-key-hereSupports tag/operation filtering (--include-tags, --exclude-tags) and an operation cap (openapi_max_operations) for large APIs.
3 OpenAPI specs ship out of the box: ClinicalTrials.gov, Finnhub, and Crossref. Additional specs are available in contrib/specs/.
Any MCP server — stdio subprocess or remote HTTP — gets its tools auto-discovered. No hand-written tool definitions.
# Remote MCP server (HTTP transport)
ati provider add-mcp linear --transport http \
--url "https://mcp.linear.app/mcp" \
--auth bearer --auth-key linear_api_key
# Local MCP server (stdio transport)
ati provider add-mcp github --transport stdio \
--command npx --args "-y" --args "@modelcontextprotocol/server-github" \
--env 'GITHUB_PERSONAL_ACCESS_TOKEN=${github_token}'
# Store the key, then use the tools
ati key set github_token ghp_your_token_here
ati run github:search_repositories --query "rust mcp"
ati run github:read_file --owner anthropics --repo claude-code --path README.mdA skill is a SKILL.md that teaches agents how to use an API — endpoints, auth patterns, parameters, workflows. On ati skill install, ATI reads the SKILL.md and uses a fast LLM call (Cerebras) to extract a full provider manifest automatically. No hand-written TOML, no OpenAPI spec needed. The SKILL.md is the only source of truth.
# From a git URL — ATI clones, reads SKILL.md, generates the manifest
ati skill install https://github.com/org/ati-skills#fal-generate
Generating manifest for 'fal' from SKILL.md...
Generated manifest for 'fal' at ~/.ati/manifests/fal.toml
Hint: run `ati key set fal_api_key <your-key>` to configure credentials.
Installed 'fal-generate' to ~/.ati/skills/fal-generate
# From a local directory — same thing, ATI copies it into ~/.ati/skills/
ati skill install ./my-skills/fal-generate/
# Either way, tools are immediately available
ati tool list
┌─────────────────────────────────────────────────────────────────────────┬──────────┬─────────────┐
│ DESCRIPTION ┆ PROVIDER ┆ TOOL │
╞═════════════════════════════════════════════════════════════════════════╪══════════╪═════════════╡
│ Submit a generation job to fal.ai queue. Returns request_id for polling. ┆ fal ┆ fal:submit │
│ Check status of a queued fal.ai job. ┆ fal ┆ fal:status │
│ Get the result of a completed fal.ai job. Returns generated media URLs. ┆ fal ┆ fal:result │
│ Cancel a queued fal.ai job. ┆ fal ┆ fal:cancel │
└─────────────────────────────────────────────────────────────────────────┴──────────┴─────────────┘
# Set the key and cook
ati key set fal_api_key sk-your-key-here
ati assist "generate a portrait photo"The skill creator writes the SKILL.md — that's it. ATI + Cerebras extracts everything: base URL, auth type, endpoints, parameters, HTTP methods. Local paths get copied into ~/.ati/skills/, git URLs get cloned — either way the manifest is auto-generated and cached in ~/.ati/manifests/.
Run gh, gcloud, kubectl, or any CLI through ATI. The agent calls ati run, ATI spawns the subprocess with a curated environment, and credentials never leak to the agent.
# Wrap the GitHub CLI
ati provider add-cli gh --command gh \
--env 'GH_TOKEN=${github_token}'
# Wrap gcloud with a credential file
ati provider add-cli gcloud --command gcloud \
--default-args "--format" --default-args "json" \
--env 'GOOGLE_APPLICATION_CREDENTIALS=@{gcp_service_account}'
# Use them
ati run gh pr list --state open --limit 5
ati run gcloud compute instances list --project my-projectThe ${key} syntax injects a keyring secret as an env var. The @{key} syntax materializes it as a temporary file (0600 permissions, wiped on process exit) — for CLIs that need a credential file path.
CLI providers get a curated environment (only PATH, HOME, TMPDIR, LANG, USER, TERM from the host). The subprocess can't see your shell's full environment.
For CLIs that produce binary outputs (screenshots, downloads, PDFs, generated media) — see the CLI handler binary output capture section below. Without it, those bytes get stranded on the proxy filesystem in proxy mode.
Example: Google Workspace — 25 APIs, one CLI. ATI ships a pre-built manifest for gws (Google Workspace CLI). It covers Drive, Gmail, Calendar, Sheets, Docs, Slides, Chat, Admin, and 18 more services — all auto-discovered from Google's Discovery Service.
# Install gws, store your service account, go
npm install -g @googleworkspace/cli
ati key set google_workspace_credentials "$(cat service-account.json)"
# Agent asks how to create a presentation
ati assist google_workspace "how do I create a new Google Slides presentation?"
# Create a new presentation with:
#
# ati run google_workspace -- slides presentations create \
# --json '{"title": "My Presentation"}'
#
# --json is required for the request body (POST). title is the only required field.
# Returns the presentation ID and URL.
#
# Check the schema first for all available fields:
# ati run google_workspace -- schema slides.presentations.create
# Agent runs it
ati run google_workspace -- slides presentations create \
--json '{"title": "Q1 Review"}'
# List Drive files, search Gmail, check calendar
ati run google_workspace -- drive files list --params '{"pageSize": 10}'
ati run google_workspace -- gmail messages list \
--params '{"userId": "me", "q": "from:ceo@company.com is:unread"}'ATI materializes the service account JSON as a temp file (0600, wiped on exit) via the @{key} syntax — the agent never sees raw credentials. In proxy mode, service accounts create files in their own invisible Drive. To have files appear in a real user's Drive, enable domain-wide delegation and set the impersonated user:
ati key set google_workspace_user analyst@company.com
# Now slides, docs, sheets are created in the analyst's DriveFor APIs where you want precise control over endpoints, parameters, and response extraction, write TOML manifests directly:
# ~/.ati/manifests/pubmed.toml
[provider]
name = "pubmed"
description = "PubMed medical literature search"
base_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
auth_type = "none"
[[tools]]
name = "medical_search"
description = "Search PubMed for medical research articles"
endpoint = "/esearch.fcgi"
method = "GET"
[tools.input_schema]
type = "object"
required = ["term"]
[tools.input_schema.properties.term]
type = "string"
description = "Search term (e.g. 'CRISPR gene therapy')"
[tools.input_schema.properties.retmax]
type = "integer"
description = "Max results"
default = 20
[tools.response]
extract = "$.esearchresult"
format = "json"ati run medical_search --term "CRISPR gene therapy" --retmax 5Auth types: bearer, header, query, basic, oauth2, none.
ATI ships with a virtual file_manager provider that exposes two tools without any manifest by default. It exists so agents in hardened sandboxes (which can only reach the ATI proxy) can still pull bytes from arbitrary URLs and push files to operator-allowlisted destinations.
# "I have a URL, give me the bytes." Writes to the sandbox's local path.
ati run file_manager:download \
--url "https://v3b.fal.media/files/b/.../output.mp4" \
--out /tmp/clip.mp4
# → {"success": true, "path": "/tmp/clip.mp4", "size_bytes": 8655798,
# "content_type": "video/mp4", "source_url": "..."}
# Or return base64 inline (small files / data pipelines)
ati run file_manager:download --url "https://example.com/report.csv" --inline true
# Push a local file to a manifest-declared destination, get a public URL.
ati run file_manager:upload --path /tmp/example.png --destination fal
# → {"success": true,
# "url": "https://v3b.fal.media/files/b/.../example.png",
# "size_bytes": 15236, "content_type": "image/png", "destination": "fal"}Download parameters: --out <path>, --inline true, --max-bytes <n> (default 500 MB), --timeout <seconds> (default 120), --headers <json>, --follow-redirects true|false.
Upload parameters: --path <local-file>, --destination <key> (one of the operator's allowlist keys; omit to use the default), --content-type <mime> (override extension-based inference), --object-name <name> (override generated object key for GCS-style sinks).
In proxy mode the proxy performs the network fetch / upload and returns base64 to the sandbox, which writes it to disk. In local mode ATI does the work directly. Both modes share the same JSON contract.
Uploads are gated by an explicit allowlist: agents can only write to destinations the operator declares in ~/.ati/manifests/file_manager.toml. Without this manifest, uploads return UploadNotConfigured. This closes the egress hole — a compromised agent cannot pick its own upload target.
Drop a single manifest:
# ~/.ati/manifests/file_manager.toml
[provider]
name = "file_manager"
description = "Generic binary download/upload"
handler = "file_manager"
upload_default_destination = "fal" # used when --destination is omitted
# fal.ai CDN — for inputs to fal models (images, video, audio)
[provider.upload_destinations.fal]
kind = "fal_storage"
key_ref = "fal_api_key" # keyring key holding the fal API key
# Google Cloud Storage bucket — for general workspace artifacts
[provider.upload_destinations.gcs]
kind = "gcs"
bucket = "my-uploads"
prefix = "ati-uploads" # optional, default "ati-uploads"
key_ref = "gcp_credentials" # optional, default "gcp_credentials"Then set the keyring keys:
ati key set fal_api_key "your-fal-key"
ati key set gcp_credentials "@file:/path/to/gcp-sa.json"Now agents call file_manager:upload --destination fal (or --destination gcs); anything else returns UnknownDestination with HTTP 403. Add a destination by adding another [provider.upload_destinations.<name>] block — no code changes needed.
The manifest validator refuses inconsistent configs (e.g. an upload_default_destination that isn't in the destinations map) at load time.
Supported sink kinds:
kind |
Required fields | What it does |
|---|---|---|
gcs |
bucket, key_ref (default gcp_credentials), prefix (default ati-uploads) |
Uploads to <bucket>/<prefix>/<utc-date>/<uuid>-<filename> via the GCS JSON API. Returns https://storage.googleapis.com/<bucket>/<obj>. |
fal_storage |
key_ref (default fal_api_key), endpoint (optional, default https://rest.alpha.fal.ai) |
Mints a signed CDN token via fal's /storage/auth/token endpoint, then uploads via POST /files/upload with X-Fal-File-Name. Returns the access_url fal hands back. |
ATI_SSRF_PROTECTION=1— blocks downloads to private/internal addresses (loopback, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.).ATI_DOWNLOAD_ALLOWLIST=v3b.fal.media,*.googleapis.com,raw.githubusercontent.com— comma-separated host patterns the proxy is allowed to fetch from. Supports*.example.comsubdomain wildcards. When unset, downloads are unrestricted — only acceptable in local-mode dev. Always set this on production proxies, otherwise an agent can use the proxy to exfiltrate to attacker-controlled hosts or probe internal infrastructure. Suffix-collision tricks likeevilfal.mediaagainst*.fal.mediaare correctly rejected (only true subdomain matches pass).
CLI providers (handler = "cli") wrap an external binary and ship its stdout back to the agent. That works fine for tools that print JSON — but most CLIs that produce binary output (screenshots, PDFs, downloaded assets, generated media) write to a file instead. Without help, those bytes land on the proxy filesystem, unreachable to the sandbox agent that called the tool.
The fix: declare which args designate output files. The proxy substitutes proxy-side temp paths, runs the subprocess, reads the resulting bytes, and ships them back to the sandbox where the CLI writes them to the path the agent originally asked for.
Two manifest fields:
# manifests/parcha-tools-mcp.toml (excerpt — works on any CLI provider)
[provider]
name = "bb"
handler = "cli"
cli_command = "bb"
# Named flags whose value is an output path (--output, -o, --out, ...).
cli_output_args = ["--output", "-o"]
# Subcommand prefix → 0-based positional arg index that's an output path.
# Example: `bb browse screenshot /tmp/x.png` — arg 0 of args after the prefix.
[provider.cli_output_positional]
"browse screenshot" = 0
"browse pdf" = 0What this enables:
# Agent inside a sandbox calls bb through the proxy.
ATI_PROXY_URL=http://proxy.greppy3.parcha.dev ati run bb browse screenshot /tmp/shot.png
# Behind the scenes:
# 1. Sandbox CLI sends raw_args verbatim to proxy
# 2. Proxy detects "browse screenshot" matches → arg 0 is an output path
# 3. Proxy rewrites /tmp/shot.png → /tmp/.ati-cli-out-<pid>-<rand>.png
# 4. Proxy runs `bb browse screenshot /tmp/.ati-cli-out-<pid>-<rand>.png`
# 5. Proxy reads the resulting PNG, base64s it into the response
# 6. Sandbox CLI decodes and writes to /tmp/shot.png inside the sandbox
# Response shape:
# {
# "stdout": "<whatever bb printed>",
# "outputs": {
# "/tmp/shot.png": {
# "path": "/tmp/shot.png",
# "size_bytes": 15236,
# "content_type": "image/png"
# }
# }
# }
# (content_base64 is stripped from the final response — file is already on disk.)Backward compatible. CLI manifests without cli_output_args / cli_output_positional produce the existing flat string/JSON response unchanged. Both forms — --output <path> and --output=<path> — are recognized. Multiple output args in one invocation are all captured.
Safety:
- Failed subprocess (non-zero exit, timeout, spawn failure) cleans up temp files unconditionally.
ATI_CLI_MAX_OUTPUT_BYTES(default 500 MB) caps each captured file. Exceeding returns a typedOutputTooLargeerror, never a panic.- Missing output file (subprocess succeeded but didn't write what we expected) returns a typed
OutputMissingerror rather than silently dropping the call. - Temp file extension is preserved from the agent's original path so CLIs that key behavior off file extension still work.
Combine with file_manager. Common pattern for media agents: bb browse screenshot /tmp/shot.png → file_manager:upload --path /tmp/shot.png --destination fal → https://v3b.fal.media/.../shot.png URL ready to feed into a fal model.
Every provider is a .toml file in ~/.ati/manifests/. The ati provider commands generate these for you, but you can also edit them directly for full control.
# What you've got
ati provider list
ati provider info github
# Add via CLI
ati provider add-mcp ...
ati provider add-cli ...
ati provider import-openapi ...
# Or edit manifests directly
$EDITOR ~/.ati/manifests/my-provider.toml
# Remove
ati provider remove my-providerATI ships with 8 curated manifests covering the main provider types — HTTP, OpenAPI, MCP, and CLI. Use them as-is or as templates for your own. Additional manifests are available in contrib/.
Three tiers of finding the right tool.
$ ati tool search "sanctions"
PROVIDER TOOL DESCRIPTION
complyadvantage ca_person_sanctions_search Search sanctions lists for individuals
complyadvantage ca_business_sanctions_search Search sanctions lists for businesses
$ ati tool search "stock price"
PROVIDER TOOL DESCRIPTION
finnhub finnhub_quote Get real-time stock quoteFuzzy search across tool names, descriptions, providers, categories, tags, and hints.
$ ati tool info clinicaltrials:searchStudies
Tool: clinicaltrials:searchStudies
Provider: clinicaltrials (ClinicalTrials.gov API)
Handler: openapi
Endpoint: GET https://clinicaltrials.gov/api/v2/studies
Description: Search clinical trial studies
Tags: Clinical Trials
Input Schema:
--query.term (string) **required**: Search term
--filter.overallStatus (string): Filter by overall study status
--filter.phase (string): Filter by study phase
--pageSize (integer, default: 10): Results per page
Usage:
ati run clinicaltrials:searchStudies --query.term "cancer" --pageSize 10ati assist answers naturally — which tools, what order, gotchas — not a numbered command list.
# Broad — searches all tools
$ ati assist "How do I screen a person for sanctions?"
# For sanctions screening, use `ca_person_sanctions_search`:
#
# ati run ca_person_sanctions_search --search_term "John Smith" --fuzziness 0.6
#
# Set fuzziness between 0.4–0.8 depending on how strict you need matching.
# You'll also want to check PEP lists in the same pass:
#
# ati run ca_person_pep_search --search_term "John Smith" --fuzziness 0.6
# Scoped to a provider — captures --help output for CLIs
$ ati assist gh "how do I create a pull request?"
# Use the `pr create` subcommand:
#
# ati run gh -- pr create --title "Fix login bug" --body "Resolves #123"
#
# Your branch must be pushed to the remote first. Use `--base main` to
# target a specific branch, or `--draft` to open as a draft PR.
# Scoped to a tool — uses the full schema for precise commands
$ ati assist github:search_repositories "search private repos only"Three tiers of credential protection, matched to your threat model. All use the same ati run interface.
Quick, no ceremony. For local development.
ati key set github_token ghp_abc123
ati key set finnhub_api_key your-key-here
ati key list # values maskedStored at ~/.ati/credentials with 0600 permissions. Also supports ATI_KEY_ env var prefix.
AES-256-GCM encrypted keyring. The orchestrator provisions a one-shot session key to /run/ati/.key (deleted after first read). Keys held in mlock'd memory, zeroized on drop.
┌─────────────────────────────────────────────────────┐
│ Sandbox │
│ │
│ ┌──────────┐ ati run my_tool ┌──────────┐ │
│ │ Agent │ ────────────────────────▶│ ATI │ │
│ │ │ │ binary │ │
│ │ │◀────────────────────────│ │ │
│ └──────────┘ structured result └────┬─────┘ │
│ │ │
│ reads encrypted keyring ───┘ │
│ injects auth headers │
│ enforces scopes │
│ │
│ /run/ati/.key (session key, deleted after read) │
└─────────────────────────────────────────────────────┘
ATI forwards all calls to a central proxy server holding the real keys. The sandbox never touches credentials.
┌──────────────────────────┐ ┌────────────────────────────┐
│ Sandbox │ │ Proxy Server (ati proxy) │
│ │ │ │
│ Agent → ATI binary ─────│── POST ─│──▶ keyring + MCP servers │
│ │ /call │ injects auth │
│ No keys. No keyring. │ /mcp │ routes by tool name │
│ Only manifests + JWT. │ │ calls upstream APIs │
└──────────────────────────┘ └────────────────────────────┘
Switch modes with one env var — the agent never changes its commands:
# Local mode (default)
ati run my_tool --arg value
# Proxy mode — same command, routed to proxy
export ATI_PROXY_URL=http://proxy-host:8090
ati run my_tool --arg value| Dev Mode | Local Mode | Proxy Mode | |
|---|---|---|---|
| Credentials | Plaintext file | Encrypted keyring | Not in sandbox |
| Key exposure | Readable on disk | mlock'd memory | Never enters sandbox |
| Setup | ati key set |
Keyring + session key | ATI_PROXY_URL |
| Use case | Local dev | Sandboxed agents | Untrusted sandboxes |
Each agent session gets a JWT with identity, permissions, and an expiry. The proxy validates on every request — agents only access what they're granted.
| Scope | Grants |
|---|---|
tool:web_search |
One specific tool |
tool:github:* |
Wildcard — all GitHub MCP tools |
help |
Access to ati assist / proxy /help endpoint |
skill:compliance-screening |
A specific skill (visible in /skills and assist context) |
* |
Everything (dev/testing only) |
Scopes also control discovery: /tools, /skills, and MCP tools/list only return what the caller's JWT allows. An agent with tool:web_search cannot enumerate any other tools or skills.
# Generate signing keys
ati token keygen ES256 # Asymmetric (production)
ati token keygen HS256 # Symmetric (simpler)
# Issue a scoped token
ati token issue \
--sub agent-7 \
--scope "tool:web_search tool:github:* help" \
--ttl 3600
# Inspect / validate
ati token inspect $ATI_SESSION_TOKEN
ati token validate $ATI_SESSION_TOKENA compliance agent gets tool:ca_* and skill:compliance-screening. A research agent gets tool:arxiv_* and tool:deepwiki:*. Neither can access the other's tools.
Skills are methodology documents that teach agents how to approach a task — when to use which tools, how to interpret results, what workflow to follow.
Tools provide data access. Skills provide workflow.
~/.ati/skills/compliance-screening/
├── skill.toml # Metadata: tool bindings, keywords, dependencies
└── SKILL.md # The methodology document
Skills auto-activate based on the agent's tool scope. If an agent has access to ca_person_sanctions_search, ATI automatically loads the compliance-screening skill because its tools binding includes that tool. Resolution walks: tool → provider → category → depends_on transitively.
When an agent calls ati assist, skills are automatically loaded into the LLM's context — the agent gets methodology-aware recommendations, not just raw command syntax.
ati skill list # List all skills
ati skill search "sanctions" # Search by keyword
ati skill show compliance-screening # Read the methodology
ati skill read --tool fal:submit # Dump all skills for a tool (agent-optimized)
ati skill init my-skill --tools T1,T2 # Scaffold a new skill
ati skill install ./my-skill/ # Install from local dir
ati skill install https://github.com/org/repo#skill-name # Install from git URL
ati skill resolve # See what resolves for current scopes
# Remote skill access (GCS registry, no local install required)
ati skill fetch catalog # List available remote skills
ati skill fetch catalog --search "sanctions"# Fuzzy search remote catalog
ati skill fetch read compliance-screening # Read remote SKILL.md
ati skill fetch resources fal-generate # List bundled files for a skill
ati skill fetch cat fal-generate scripts/generate.sh # Read a specific file
ati skill fetch refs fal-generate # List on-demand references
ati skill fetch ref fal-generate usage.md # Read a reference file
ati skill fetch build-index ./skills/ --output-file catalog.json # Build a catalog index
ati skill fetchvsati skillati: Both invoke the same commands.ati skill fetch <cmd>is the standard entry point;ati skillati <cmd>is the standalone alias.
Store skills in a Google Cloud Storage bucket for centralized management. The proxy serves them to agents on demand — agents don't need GCS credentials.
Two access modes:
- Via proxy — agents call
/skillati/*endpoints; the proxy fetches lazily from GCS on first access, caching results in memory for the session. Local~/.ati/skills/skills are always available via/skills; GCS skills are served separately via/skillati/. - Direct (local mode) —
ati skill fetch/ati skillaticalls GCS directly using credentials from the local keyring.
Setup:
-
Create a GCS bucket and upload skills (each skill is a directory):
gs://my-skills-bucket/ ├── compliance-screening/ │ ├── SKILL.md │ └── skill.toml ├── fal-generate/ │ ├── SKILL.md │ ├── skill.toml │ ├── provider.toml │ └── scripts/ │ ├── generate.sh │ └── get-schema.sh └── ...Optionally generate a fast-path catalog index for O(1) discovery:
ati skill fetch build-index ./skills/ --output-file catalog.json # Upload catalog.json as gs://my-skills-bucket/_skillati/catalog.v1.json -
Add GCP service account credentials to the ATI keyring:
ati key set gcp_credentials < /path/to/service-account.json
-
Start the proxy with the registry env var:
ATI_SKILL_REGISTRY=gcs://my-skills-bucket ati proxy --port 8090
The proxy reads remote skills lazily on first access and caches them for the session. Local ~/.ati/skills/ is always loaded at startup; GCS skills layer on top and are fetched on demand via the /skillati/* endpoints.
Python SDK — downloading skills for agent sandboxes:
from ati import AtiOrchestrator
orch = AtiOrchestrator(proxy_url="http://proxy:8090", secret=secret)
# Provision a sandbox with skill content
env = orch.provision_sandbox(
agent_id="agent-1",
tools=["ca_person_sanctions_search"],
skills=["compliance-screening"],
fetch_skill_content=True,
)
# env["skills"] = {"compliance-screening": "# Compliance Screening\n..."}
# Or download full skill directories (SKILL.md + scripts, references, etc.)
orch.download_skills(
["fal-generate", "compliance-screening"],
dest_dir=".claude/skills",
token=env["ATI_SESSION_TOKEN"],
)
# Creates .claude/skills/fal-generate/SKILL.md, scripts/generate.sh, etc.| Endpoint | Description |
|---|---|
GET /skills |
List installed (local) skills |
GET /skills/:name/bundle |
Download full skill directory as JSON |
POST /skills/resolve |
Resolve skills for given scopes (with optional content) |
GET /skillati/catalog |
List remote GCS skills (optionally ?search=query) |
GET /skillati/:name |
Read SKILL.md for a remote skill |
GET /skillati/:name/resources |
List bundled files for a remote skill |
GET /skillati/:name/file?path=... |
Read an arbitrary file from a remote skill |
GET /skillati/:name/refs |
List on-demand reference files |
GET /skillati/:name/ref/:ref |
Read a specific reference file |
This is a real workflow an agent ran using ATI — three fal.ai models chained together, guided by skills. The agent generated an image, synthesized speech, and produced a lip-synced talking head video.
The result: fal-lipsync.mp4
ati run fal:submit \
--endpoint_id "fal-ai/flux/dev" \
--prompt "Gen Z female tech streamer, colorful RGB lighting, confident smile" \
--image_size "portrait_4_3"
# request_id: 1d491d8e-5c22-417b-a62b-471aa7f380e3
# status: IN_QUEUE
ati run fal:result --endpoint_id "fal-ai/flux" \
--request_id "1d491d8e-5c22-417b-a62b-471aa7f380e3"
# images: [{url: "https://v3b.fal.media/files/.../streamer.jpg"}]ati run fal:submit \
--endpoint_id "fal-ai/elevenlabs/tts/eleven-v3" \
--text "Check out ATI — one binary, every tool your agent needs." \
--voice_id "cjVigY5qzO86Huf0OWal"
# request_id: f9b24972-9ea9-47bd-9e6c-1fc8f48c70c5
ati run fal:result --endpoint_id "fal-ai/elevenlabs" \
--request_id "f9b24972-9ea9-47bd-9e6c-1fc8f48c70c5"
# audio: {url: "https://v3b.fal.media/files/.../output.mp3"}ati run fal:submit \
--endpoint_id "veed/fabric-1.0" \
--image_url "https://v3b.fal.media/files/.../streamer.jpg" \
--audio_url "https://v3b.fal.media/files/.../output.mp3" \
--resolution "720p"
# request_id: 1c7bdab9-3572-45fe-829d-c5c87071e7d9
ati run fal:result --endpoint_id "veed/fabric-1.0" \
--request_id "1c7bdab9-3572-45fe-829d-c5c87071e7d9"
# video: {url: "https://v3b.fal.media/files/.../lipsync.mp4"}The agent didn't know any of this workflow. It asked ati assist:
ati assist fal "I want to create a lip-synced talking head video"
# To create a lip-synced talking head video, you'll use the VEED Fabric 1.0
# model on fal.ai. This requires:
# - A face image URL (portrait/headshot)
# - An audio URL (speech)
#
# Step 1: Generate speech with ElevenLabs TTS...
# Step 2: Submit the lip-sync job to veed/fabric-1.0...
# Step 3: Poll status and get result...
#
# Best Practices:
# Face Image: Front-facing, good lighting, neutral expression
# Audio: Clean speech, no background noise
# Duration: Keep under 60 seconds per segmentati assist loaded skills for fal-generate, elevenlabs-tts-api, and veed-fabric-lip-sync — giving the agent model-specific best practices, not just raw command syntax.
Providers can declare associated skills in their manifest. When an agent imports a provider, it can install the skills in one command:
# ~/.ati/manifests/fal.toml
[provider]
name = "fal"
base_url = "https://queue.fal.run"
skills = [
"https://github.com/org/ati-skills#fal-generate",
"https://github.com/org/ati-skills#veed-fabric-lip-sync",
]# See what skills are declared
ati provider info fal
# Skills: 2 declared (1 installed, 1 not installed)
# Install: ati provider install-skills fal
# Install all declared skills
ati provider install-skills falIf your framework has a shell tool, ATI works. The pattern is always the same — system prompt + shell access. No custom tool wrappers, no SDK-specific adapters.
system_prompt = """
You have ATI on your PATH. Available commands:
- `ati tool search <query>` — find tools by keyword
- `ati tool info <name>` — inspect a tool's schema
- `ati run <tool> --key value` — execute a tool
- `ati assist "<question>"` — ask which tools to use and how
"""
# Claude Agent SDK
agent = Agent(model="claude-sonnet-4-20250514", tools=[Bash()], system=system_prompt)
# OpenAI Agents SDK
agent = Agent(model="gpt-4o", tools=[shell_tool], instructions=system_prompt)
# LangChain
agent = create_react_agent(llm, [ShellTool()], prompt=system_prompt)| SDK | Shell Mechanism | Example |
|---|---|---|
| Claude Agent SDK | Built-in Bash tool |
~100 lines |
| OpenAI Agents SDK | @function_tool async shell |
~100 lines |
| Google ADK | run_shell() function tool |
~120 lines |
| LangChain | ShellTool (zero-config) |
~90 lines |
| Codex CLI | Built-in shell agent | ~60 lines |
| Pi | Built-in bashTool |
~100 lines |
Every example uses free, no-auth tools so you can run them immediately with just an LLM API key. See examples/.
For production, run ati proxy as a central server holding secrets:
ati proxy --port 8090 --ati-dir ~/.ati # From credentials file
ati proxy --port 8090 --ati-dir ~/.ati --env-keys # From env vars
ati init --proxy --es256 # Initialize with JWT keys| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Status — tool/provider/skill counts |
/call |
POST | Execute tool — {tool_name, args} |
/mcp |
POST | MCP JSON-RPC pass-through |
/help |
POST | LLM-powered tool guidance (help scope required) |
/tools |
GET | List/search visible tools (scope-filtered) |
/tools/:name |
GET | Tool detail |
/skills |
GET | List/search installed skills (scope-filtered) |
/skills/:name |
GET | Skill content and metadata |
/skills/:name/bundle |
GET | Full skill directory as JSON |
/skills/resolve |
POST | Resolve skills for scopes |
/skillati/catalog |
GET | List remote GCS skills (?search=query) |
/skillati/:name |
GET | Read remote SKILL.md |
/skillati/:name/resources |
GET | List remote skill files |
/skillati/:name/file |
GET | Read a remote skill file (?path=...) |
/skillati/:name/refs |
GET | List remote reference files |
/skillati/:name/ref/:ref |
GET | Read a remote reference file |
/.well-known/jwks.json |
GET | JWKS public key |
All endpoints except /health and JWKS require Authorization: Bearer <JWT> when JWT is configured. Tool and skill listing is scope-filtered — callers only see what their JWT allows.
For production deployments that want a per-call audit log and revocable virtual
keys, ATI can persist to Postgres. It's opt-in: if you don't set
ATI_DB_URL, the proxy works exactly as documented above with no DB connection.
# Build (or pull) ATI with the `db` feature
cargo build --release --features db
# Point at any PostgreSQL 13+ database with full DML privileges
export ATI_DB_URL="postgres://ati:<password>@db:5432/ati"
# `--migrate` applies embedded migrations on startup — safe and idempotent
ati proxy --port 8090 --migrate/health will report db: "connected" once the pool is up. See
docs/PERSISTENCE.md for the full schema, configuration
reference, failure semantics, and operational guidance (backups, rolling
deploys, schema drift).
The ati-client Python package provides orchestrator provisioning and JWT token utilities for integrating ATI into Python orchestrators.
pip install ati-clientfrom ati import AtiOrchestrator
orch = AtiOrchestrator(
proxy_url="https://ati-proxy.example.com",
secret="17332cf135d362f79a2ed700b13e1215978be1d6ae6e133d25b6b3f21fa10299",
)
# One call — returns env vars to inject into the sandbox
env_vars = orch.provision_sandbox(
agent_id=f"sandbox:{sandbox_id}",
tools=["finnhub_quote", "web_search", "github:*"],
skills=["financial-analysis"],
ttl_seconds=7200,
rate={"tool:github:*": "10/hour"},
)
# env_vars = {"ATI_PROXY_URL": "...", "ATI_SESSION_TOKEN": "eyJ..."}from ati import issue_token, validate_token, inspect_token
token = issue_token(
secret="17332cf135d362f79a...",
sub="agent-7",
scope="tool:web_search tool:finnhub_quote",
ttl_seconds=3600,
)
claims = validate_token(token, secret="17332cf135d362f79a...")
print(claims.sub) # "agent-7"
print(claims.scopes()) # ["tool:web_search", "tool:finnhub_quote"]Tokens are HS256-signed JWTs fully compatible with the Rust proxy — tested bidirectionally. See ati-client/python/ for full docs.
ati — Agent Tools Interface
COMMANDS:
init Initialize ATI directory structure (~/.ati/)
run Execute a tool by name
tool List, inspect, search, and discover tools
provider Add, list, remove, inspect, and import providers
skill Manage skills (methodology docs for agents)
skillati Lazily read remote skills from the GCS registry (alias for `skill fetch`)
assist Ask which tools to use and how (LLM-powered)
key Manage API keys in the credentials store
token JWT token management (keygen, issue, inspect, validate)
auth Show authentication and scope information
proxy Run ATI as a proxy server
version Print version information
OPTIONS:
--output <FORMAT> json, table, text [default: text]
--verbose Enable debug output
ati provider import-openapi <spec> [--name NAME] [--include-tags T1,T2] [--dry-run]
ati provider inspect-openapi <spec> [--include-tags T1,T2]
ati provider add-mcp <name> --transport stdio|http [--command CMD] [--url URL] [--env 'KEY=${ref}']
ati provider add-cli <name> --command CMD [--default-args ARG] [--env 'KEY=${ref}'] [--env 'KEY=@{ref}']
ati provider list
ati provider info <name>
ati provider remove <name>ati key set <name> <value> # Store a key
ati key list # List (values masked)
ati key remove <name> # Delete
ati token keygen ES256|HS256 # Generate signing key
ati token issue --sub ID --scope "..." --ttl SECONDS # Issue scoped JWT
ati token inspect <token> # Decode without verification
ati token validate <token> [--key path|--secret hex] # Full verificationati run finnhub_quote --symbol AAPL # Human-readable text (default)
ati --output json run finnhub_quote --symbol AAPL # JSON for programmatic use
ati --output table run finnhub_quote --symbol AAPL # Table for tabular data| Variable | Description |
|---|---|
ATI_PROXY_URL |
Enable proxy mode — forward all calls to this URL |
ATI_SESSION_TOKEN |
JWT Bearer token for proxy auth (carries scopes) |
ATI_DIR |
Override ATI directory (default: ~/.ati) |
ATI_KEY_FILE |
Override session key path (default: /run/ati/.key) |
ATI_OUTPUT |
Default output format: json, table, text |
ATI_JWT_PUBLIC_KEY |
Path to ES256 public key PEM (proxy JWT validation) |
ATI_JWT_PRIVATE_KEY |
Path to ES256 private key PEM (token issuance) |
ATI_JWT_SECRET |
Hex-encoded HS256 shared secret |
ATI_JWT_ISSUER |
Expected iss claim in JWTs (optional) |
ATI_JWT_AUDIENCE |
Expected aud claim (default: ati-proxy) |
ATI_SSRF_PROTECTION |
SSRF protection: 1/true to block, warn to log only |
ATI_SKILL_REGISTRY |
Remote skill registry: gcs://my-bucket (direct GCS) or proxy (fetch through ATI_PROXY_URL) |
ATI_SKILL_REGISTRY_INDEX_OBJECT |
Override catalog index path in the GCS bucket (default: _skillati/catalog.v1.json) |
RUST_LOG |
Tracing log level, e.g. debug, ati=debug |
cargo build # Debug
cargo build --release # Release
cargo build --release --target x86_64-unknown-linux-musl # Static binary (no glibc)
cargo test # 663 tests
bash scripts/test_skills_e2e.sh # Skill e2e tests
cargo test --test mcp_live_test -- --ignored # Live MCP tests (needs API keys)ati/
├── Cargo.toml
├── manifests/ # 8 curated provider manifests (HTTP, MCP, OpenAPI, CLI)
├── specs/ # 3 OpenAPI specs for curated providers
├── contrib/ # 35+ additional manifests and specs (gitignored)
├── skills/ # Skill methodology documents
├── examples/ # 6 SDK integrations (Claude, OpenAI, ADK, LangChain, Codex, Pi)
├── ati-client/python/ # Python SDK (pip install ati-client)
├── scripts/ # E2E test scripts
├── docs/
│ ├── SECURITY.md # Threat model and security design
│ ├── OTEL.md # OpenTelemetry export — env vars, Grafana Cloud quickstart
│ └── IDEAS.md # Future directions
├── src/
│ ├── main.rs # CLI entry point (clap)
│ ├── lib.rs # Library crate
│ ├── cli/ # Command handlers
│ ├── core/ # Registry, MCP client, OpenAPI parser, HTTP executor,
│ │ # keyring, JWT, scopes, skills, response processing
│ ├── proxy/ # Client + server (axum)
│ ├── security/ # mlock/madvise/zeroize, sealed key file
│ └── output/ # JSON, table, text formatters
└── tests/ # Unit, integration, e2e, live MCP
Apache-2.0
