ATI (Agent Tools Interface) is designed to give AI agents access to external tools without exposing API keys. It supports two execution modes with different security properties.
Keys are encrypted and provisioned into the sandbox. ATI decrypts them in process memory, makes API calls directly, and the keys never appear in environment variables or readable files.
Trade-off: Keys ARE in the sandbox (encrypted, memory-locked), but no external infrastructure needed.
Zero credentials in the sandbox. ATI forwards tool calls to an external proxy server that holds the real API keys. The sandbox never sees any key material.
Trade-off: Stronger isolation, but requires running a proxy server and adds a network hop.
ati run checks the ATI_PROXY_URL environment variable:
- Set → proxy mode (forward to proxy server)
- Not set → local mode (use keyring)
The agent never needs to know which mode is active.
- Orchestrator generates a random 256-bit session key per sandbox
- Orchestrator encrypts scoped API keys with session key →
keyring.enc - Session key written to
/run/ati/.keyon tmpfs (mode 0400) keyring.enc+ tool manifests uploaded to sandbox
- ATI reads
/run/ati/.key, immediatelyunlink()s the file - ATI decrypts
keyring.encwith AES-256-GCM mlock()on decrypted memory (prevents swap-out)- Session key zeroed from stack
- Keys exist only in ATI's locked heap
- Algorithm: AES-256-GCM (authenticated encryption with associated data)
- Session key: 256-bit random, unique per sandbox session
- Nonce: 96-bit random, prepended to ciphertext
- Format:
[12-byte nonce][ciphertext + 16-byte GCM tag] - Rust crate:
aes-gcmv0.10
| Attack Vector | Mitigation |
|---|---|
env / printenv |
No secrets in environment variables |
cat /run/ati/.key |
File deleted after first read |
strings /usr/local/bin/ati |
Binary contains only encrypted blobs |
cat ~/.ati/keyring.enc |
Encrypted; session key is gone |
/proc/$(pgrep ati)/environ |
Clean — no secret env vars |
/proc/$(pgrep ati)/mem |
Requires ptrace — blocked by seccomp |
strace ati run ... |
Blocked by sandbox seccomp (no ptrace) |
| Memory dump / core dump | madvise(DONTDUMP) excludes secret pages |
| Swap file | mlock() prevents swap-out |
| Agent imports ATI internals | ATI is compiled Rust binary, not importable |
- If the sandbox has no seccomp profile,
ptracecan read ATI's memory - If the agent can run arbitrary code with sufficient privileges, it could theoretically extract keys from ATI's process memory during execution
- The session key exists in
/run/ati/.keybriefly before ATI reads it — a race condition is theoretically possible mlock()is best-effort on some kernels (may silently fail if RLIMIT_MEMLOCK is low)
- Orchestrator starts the proxy server:
ati proxy --port 8090 --ati-dir /path/to/ati - Orchestrator sets
ATI_PROXY_URLin the sandbox environment - ATI sends
POST /callwith{tool_name, args}to the proxy - Proxy server validates the request, injects API keys from its keyring, calls the upstream API
- Proxy returns the result to ATI in the sandbox
The proxy server is built into the same ati binary — run ati proxy to start it. It loads manifests and keyring from its own ATI directory.
- ATI binary
- Tool manifests (
.tomlfiles) — tool definitions only, no secrets - Scope config (
scopes.json) — which tools are allowed ATI_PROXY_URL— URL of the proxy server
- No
keyring.enc - No session key (
.keyfile) - No API keys in any form (encrypted or otherwise)
| Attack Vector | Mitigation |
|---|---|
| Any local key extraction | No keys exist in the sandbox |
ATI_PROXY_URL leak |
URL is not secret; proxy validates requests |
| Man-in-the-middle on proxy | Use HTTPS for proxy URL |
| Proxy server compromise | Same risk as any credential store (Vault, etc.) |
| Agent sends malicious requests | Proxy validates JWT scopes on every request |
| Agent enumerates tools/skills it shouldn't see | Discovery endpoints are scope-filtered |
- The proxy URL is visible via
printenv— not a secret, but reveals infrastructure - Proxy server is a single point of failure; if it's down, all tool calls fail
- Extra network hop adds latency (~10-50ms per call)
- Proxy server itself must be secured — it holds the real API keys
Scope behavior is intentionally split between production and development:
- JWT configured: ATI requires a valid
ATI_SESSION_TOKENand enforces scopes strictly. - No JWT configured: ATI runs in unrestricted dev mode for local setup and testing.
When JWT validation is configured, scopes are enforced consistently across:
ati runin local mode- proxy
/call - proxy
/mcp(tools/listandtools/call) - proxy metadata and discovery endpoints (
/tools,/skills,/help)
Other scope properties:
- Scopes are generated by the orchestrator and carried in the JWT
scopeclaim - Denied tools return clear authorization errors
- Scope expiry is enforced via JWT
exp - Wildcards such as
tool:github:*are supported - Help access requires the
helpscope when JWT auth is enabled
Tool and skill listing is scope-filtered — callers only see what their JWT allows:
GET /toolsreturns only tools the caller's scopes permitGET /skillsreturns only skills reachable from the caller's tool/provider/category scopes- MCP
tools/listreturns only the scoped tool subset ati assist/POST /helpbuilds its LLM context from visible tools and skills only
This prevents scope enumeration: an agent with tool:web_search cannot discover the existence of other tools, providers, or skills.
Skills are resolved transitively from tool scopes:
skill:X→ load skill X directlytool:Y→ skills whosetoolsbinding includes Y- Tool Y's provider → skills whose
providersbinding includes that provider - Provider's category → skills whose
categoriesbinding includes that category - Any loaded skill's
depends_on→ transitively load those dependencies
Legacy underscore JWT scopes (e.g. tool:github_search_repos) are normalized to their colon-namespaced equivalents (tool:github:search_repos) for scope checking, maintaining backward compatibility without weakening enforcement.
ATI's HTTP executor optionally blocks Server-Side Request Forgery attacks via ATI_SSRF_PROTECTION:
| Mode | Behavior |
|---|---|
| Not set (default) | No SSRF checking |
warn |
Log a warning but allow the request |
1 or true |
Block requests to private/internal addresses |
When enabled, ATI resolves the target hostname and rejects requests to:
- Loopback addresses (
127.0.0.0/8,::1) - Private network ranges (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7) - Link-local addresses (
169.254.0.0/16,fe80::/10) - Other IPv6 local ranges
DNS resolution is performed at check time to cover hosts that resolve to private IPs. This applies to hand-written HTTP tools; MCP and CLI tools are not currently in scope for SSRF checking.
Two registry transports are supported via ATI_SKILL_REGISTRY:
ATI_SKILL_REGISTRY=gcs://bucket — direct GCS access:
- GCS credentials (
gcp_credentialskeyring key) are a full GCP service account JSON — treat them with the same care as any API key - In local mode, credentials come from the ATI keyring (encrypted at rest)
- In proxy mode, credentials live only on the proxy server; agent sandboxes never see them
- Remote skill content is fetched on demand and cached in memory for the session; nothing is written to disk
ATI_SKILL_REGISTRY=proxy — fetch skills through the ATI proxy:
- Requires
ATI_PROXY_URLto be set;ATI_SESSION_TOKENprovides auth - The sandbox holds zero GCS credentials — skill content is served by the proxy's
/skillati/*endpoints - Useful for network-restricted sandboxes that cannot reach GCS directly but can reach the proxy
- Skill access is still JWT-scope-gated on the proxy side
Both transports: skill content is injected into LLM context via ati assist — operators should audit skills in their registry for prompt injection risks before deploying.
- Decrypted keys held in
HashMap<String, String>withZeroizeon drop - Raw JSON bytes also zeroized on drop
mlock()prevents kernel from swapping secret pagesmadvise(MADV_DONTDUMP)excludes from core dumps- All security functions degrade gracefully on non-Linux (warning, not error)
| Scenario | Recommended Mode |
|---|---|
| Standard sandbox deployment | Local — simpler, no extra infrastructure |
| High-security / regulated workloads | Proxy — zero key exposure |
| Air-gapped or offline sandboxes | Local — no network dependency |
| Multi-tenant with shared sandboxes | Proxy — strongest isolation |
| Development / testing | Local — easiest to set up |
Every release binary is cryptographically signed and auditable. Three levels of verification:
Release binaries are attested by GitHub Actions using artifact attestations (SLSA Level 3). This cryptographically proves the binary was built from a specific commit in the official repo by the official CI workflow.
gh attestation verify ./ati --repo Parcha-ai/atiIf this fails, do not use the binary. It was not built by our CI.
Each release includes .sha256 files alongside the binaries:
sha256sum -c ati-x86_64-unknown-linux-musl.sha256ATI binaries are built with cargo-auditable, which embeds a compressed dependency manifest inside the binary itself. You can inspect what dependencies were compiled into any ATI binary:
cargo audit bin ./atiIf an attacker rebuilt the binary with injected dependencies, they show up here. This is forensic detection — the audit trail lives inside the binary, not in a separate file that could also be swapped.
ATI enforces a strict dependency policy via cargo-deny (checked on every push):
- No unknown registries — all crates must come from crates.io (blocks typosquatting via alt registries)
- No git source dependencies — prevents "point Cargo.toml at an evil fork" attacks
- Known vulnerabilities denied — checked against the RustSec advisory database
- License allowlist — only OSI-approved permissive licenses
All CI/CD workflow actions are pinned to full commit SHAs, not mutable version tags. This prevents the class of attack demonstrated by CVE-2025-30066 (tj-actions/changed-files), where an attacker force-pushed malicious code to a version tag.