# ATI Documentation

> Source-grounded reference for the Agent Tools Interface: the `ati` CLI, optional `ati proxy` server, provider manifests, JWT scopes, skills registry, and Python orchestrator SDK used to give sandboxed agents secure tool access without exposing API keys.

## Context Links

- [Agent index](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa)
- [GitHub repository](https://github.com/Parcha-ai/ati)

## Repository Metadata

- Repository: Parcha-ai/ati

- Generated: 2026-06-02T03:49:49.721Z
- Updated: 2026-06-02T04:09:10.260Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 24

## Page Index

- 01. [Overview](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/01-overview.md) - What ATI exposes (`ati run`, provider catalog, proxy mode), runtime assumptions (`~/.ati/`, `ATI_PROXY_URL`), and the shortest path from `ati init` to a scoped tool call.
- 02. [Installation](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/02-installation.md) - Pre-built release binaries (musl/static), `cargo install agent-tools-interface`, Docker image, and platform prerequisites for local vs proxy deployments.
- 03. [Quickstart](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/03-quickstart.md) - Initialize `~/.ati/`, import a no-auth OpenAPI provider, store a key, run `ati tool list` and `ati run`, and verify success with `ati tool info`.
- 04. [Execution modes](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/04-execution-modes.md) - Dev (plaintext credentials), local (AES-256-GCM keyring + one-shot session key), and proxy (`ATI_PROXY_URL`) modes: credential placement, auto-detection in `ati run`, and threat-model trade-offs.
- 05. [Providers and handlers](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/05-providers-and-handlers.md) - Handler types (`http`, `openapi`, `mcp`, `cli`, `file_manager`, `passthrough`), tool naming (`provider:tool`), auth injection, and how manifests map to dispatch in `core/http`, `mcp_client`, and `cli_executor`.
- 06. [Scopes and tool discovery](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/06-scopes-and-tool-discovery.md) - JWT `scope` claims, wildcard patterns (`tool:github:*`), scope-filtered listing, and the three discovery tiers: `ati tool search`, `ati tool info`, and `ati assist`.
- 07. [Skills and SkillATI](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/07-skills-and-skillati.md) - Skills as methodology docs vs tools as data access, progressive disclosure (metadata → SKILL.md → resources), scope-driven resolution cascade, and remote GCS registry via `/skillati/*`.
- 08. [Import OpenAPI providers](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/08-import-openapi-providers.md) - `ati provider import-openapi` and `inspect-openapi`: spec download, operation caps, tag filters, `x-ati-param-location` routing, and keyring key hints.
- 09. [Add MCP providers](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/09-add-mcp-providers.md) - `ati provider add-mcp` for stdio and HTTP transports, `${key}` env injection, MCP `tools/list` discovery, and namespaced tool calls (`provider:tool`).
- 10. [CLI and HTTP manifests](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/10-cli-and-http-manifests.md) - `ati provider add-cli`, hand-written `[[tools]]` HTTP manifests, `${key}` vs `@{key}` credential files, `cli_output_args` binary capture, and curated subprocess environments.
- 11. [Deploy proxy server](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/11-deploy-proxy-server.md) - Run `ati proxy`, bind/port/`--env-keys`, optional `--features db` persistence, passthrough and HMAC sig-verify flags, VM/systemd examples, and health/JWKS probes.
- 12. [Configure JWT and keys](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/12-configure-jwt-and-keys.md) - `ati key set/list/remove`, `ati token keygen|issue|inspect|validate`, `ati init --proxy`, per-provider session token env overrides, and orchestrator token issuance patterns.
- 13. [File manager operations](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/13-file-manager-operations.md) - `file_manager:download` and `file_manager:upload`, SSRF protection, download allowlists, upload destination kinds (`gcs`, `fal_storage`), and proxy-side base64 transfer.
- 14. [Skills registry and fetch](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/14-skills-registry-and-fetch.md) - `ati skill install|resolve`, manifest generation from SKILL.md, GCS bucket layout, `ati skill fetch` / `skillati` commands, and proxy `/skillati/*` endpoints.
- 15. [CLI reference](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/15-cli-reference.md) - Top-level `ati` subcommands (`run`, `tool`, `provider`, `skill`, `assist`, `plan`, `key`, `token`, `auth`, `proxy`, `audit`, `edge`), global flags (`--output`, `--verbose`), and output formats.
- 16. [Environment variables](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/16-environment-variables.md) - Runtime env contract: `ATI_PROXY_URL`, `ATI_SESSION_TOKEN`, `ATI_DIR`, JWT keys, SSRF/download allowlists, skill registry, OTel export, and optional `ATI_DB_URL` / `ATI_ADMIN_TOKEN`.
- 17. [Proxy API reference](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/17-proxy-api-reference.md) - HTTP routes on `ati proxy`: `/call`, `/mcp`, `/help`, `/tools`, `/skills`, `/skillati/*`, `/health`, JWKS, optional `/admin/keys/*`, auth requirements, and request/response shapes.
- 18. [Manifest reference](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/18-manifest-reference.md) - TOML schema for `[provider]` and `[[tools]]`: `handler`, auth types, MCP/OpenAPI/CLI/passthrough fields, response `extract`/`format`, overrides, and validation errors at load time.
- 19. [JWT and scopes reference](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/19-jwt-and-scopes-reference.md) - Claims (`sub`, `aud`, `scope`, `exp`, `jti`), ES256 vs HS256, scope grammar (`tool:`, `skill:`, `help`, `*`), discovery filtering, and validation failure modes.
- 20. [Assist and plan reference](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/20-assist-and-plan-reference.md) - `ati assist` flags (`--plan`, `--save`, `--local`), internal `_llm` provider, structured plan output, and `ati plan` execution of saved tool-call plans.
- 21. [OpenAPI stock research workflow](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/21-openapi-stock-research-workflow.md) - End-to-end Finnhub recipe: `import-openapi`, `ati key set`, `ati assist`, and chained `ati run` calls for quotes, insiders, and sentiment with expected JSON fields.
- 22. [Agent harness sandbox](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/22-agent-harness-sandbox.md) - Shell-first integration pattern across SDK examples, `AtiOrchestrator.provision_sandbox`, scoped env injection, and proxy-mode agent loops without custom tool wrappers.
- 23. [Security and production](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/23-security-and-production.md) - Threat model, proxy hardening (JWT enforce, download allowlist, SSRF), sig-verify modes, optional Postgres audit/virtual keys, OTel observability, and edge keyring rotation.
- 24. [Build, test, and troubleshooting](https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/24-build-test-and-troubleshooting.md) - `cargo build/test` targets, musl release builds, feature flags (`db`, `otel`, `sentry`), E2E shell scripts, live MCP tests, and common failure signals from integration tests.

## Source File Index

- `AGENTS.md`
- `ati-client/python/README.md`
- `ati-client/python/src/ati/__init__.py`
- `ati-client/python/src/ati/scope.py`
- `ati-client/python/src/ati/token.py`
- `ati-client/python/tests/test_orchestrator.py`
- `Cargo.toml`
- `deny.toml`
- `deploy/Dockerfile`
- `deploy/examples/vm/README.md`
- `deploy/examples/vm/systemd/ati.service`
- `docs/assist-examples.md`
- `docs/JWT_STANDARDS_2026.md`
- `docs/OTEL.md`
- `docs/PERSISTENCE.md`
- `docs/SECURITY.md`
- `examples/claude-agent-sdk/openapi_agent.py`
- `examples/openai-agents-sdk/openapi_agent.py`
- `examples/README.md`
- `manifests/_llm.toml`
- `manifests/clinicaltrials.toml`
- `manifests/deepwiki-mcp.toml`
- `manifests/finnhub.toml`
- `manifests/github-mcp.toml`
- `manifests/google-workspace.toml`
- `manifests/hackernews.toml`
- `manifests/README.md`
- `migrations/20260501000001_init_persistence.sql`
- `README.md`
- `scripts/e2e/README.md`
- `scripts/test_proxy_e2e.sh`
- `scripts/test_proxy_server_e2e.sh`
- `scripts/test_skill_fetch_e2e.sh`
- `scripts/test_skills_e2e.sh`
- `skills/google-workspace/SKILL.md`
- `skills/google-workspace/skill.toml`
- `specs/clinicaltrials.json`
- `specs/crossref.json`
- `specs/finnhub.json`
- `src/cli/audit.rs`
- `src/cli/call.rs`
- `src/cli/cli_capture.rs`
- `src/cli/edge.rs`
- `src/cli/file_manager.rs`
- `src/cli/help.rs`
- `src/cli/init.rs`
- `src/cli/keys.rs`
- `src/cli/mod.rs`
- `src/cli/plan.rs`
- `src/cli/provider.rs`
- `src/cli/skillati.rs`
- `src/cli/skills.rs`
- `src/cli/token.rs`
- `src/cli/tools.rs`
- `src/core/cli_executor.rs`
- `src/core/error.rs`
- `src/core/file_manager.rs`
- `src/core/gcs.rs`
- `src/core/http.rs`
- `src/core/jwt.rs`
- `src/core/keyring.rs`
- `src/core/manifest.rs`
- `src/core/mcp_client.rs`
- `src/core/openapi.rs`
- `src/core/otel.rs`
- `src/core/passthrough.rs`
- `src/core/response.rs`
- `src/core/scope.rs`
- `src/core/sig_verify.rs`
- `src/core/skill.rs`
- `src/core/skillati.rs`
- `src/core/token.rs`
- `src/lib.rs`
- `src/main.rs`
- `src/output/mod.rs`
- `src/proxy/client.rs`
- `src/proxy/server.rs`
- `src/security/memory.rs`
- `src/security/sealed_file.rs`
- `tests/file_manager_proxy_test.rs`
- `tests/manifest_test.rs`
- `tests/mcp_live_test.rs`
- `tests/plan_test.rs`
- `tests/proxy_skills_test.rs`
- `tests/proxy_test.rs`

---

## 01. Overview

> What ATI exposes (`ati run`, provider catalog, proxy mode), runtime assumptions (`~/.ati/`, `ATI_PROXY_URL`), and the shortest path from `ati init` to a scoped tool call.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/01-overview.md
- Generated: 2026-06-02T03:13:48.010Z

### Source Files

- `README.md`
- `AGENTS.md`
- `src/main.rs`
- `src/cli/call.rs`
- `Cargo.toml`
- `src/lib.rs`

---
title: "Overview"
description: "What ATI exposes (`ati run`, provider catalog, proxy mode), runtime assumptions (`~/.ati/`, `ATI_PROXY_URL`), and the shortest path from `ati init` to a scoped tool call."
---

ATI is a single Rust binary (`ati`, crate `agent-tools-interface`) that exposes one execution surface for agents: `ati run <tool> [--key value]`. The binary loads provider manifests from the ATI directory, resolves credentials from a local keyring or a remote proxy, dispatches by handler type (`http`, `openapi`, `mcp`, `cli`, `file_manager`, `passthrough`), and formats results as JSON, table, or text. When `ATI_PROXY_URL` is set, the same command forwards to `POST /call` on an `ati proxy` server that holds secrets and enforces JWT scopes.

## What ATI exposes

| Surface | Role |
|--------|------|
| `ati run` | Execute a named tool with `--key value` arguments (flags without values become `true`) |
| `ati tool` | List, search (`ati tool search`), and inspect schemas (`ati tool info`) |
| `ati provider` | Import OpenAPI specs, add MCP/CLI providers, list/remove manifests |
| `ati assist` | LLM-guided discovery — which tools and exact `ati run` commands |
| `ati skill` / `ati skillati` | Methodology docs, install, resolve, remote GCS fetch |
| `ati key` / `ati token` | Credential storage and JWT lifecycle |
| `ati proxy` | Axum server: `/call`, `/mcp`, `/help`, `/tools`, `/skills`, `/skillati/*`, `/health` |
| `ati init` | Create `~/.ati/` layout (`manifests/`, `specs/`, `skills/`, `config.toml`) |

The clap entry point in `src/main.rs` wires subcommands to `src/cli/*` handlers. The library crate (`src/lib.rs`) publishes `core`, `proxy`, and `security` for embedding and integration tests; CLI formatting and provider glue stay binary-only.

### Global CLI behavior

<ParamField body="--output" type="json | table | text">
Default `json`. Also set via `ATI_OUTPUT` or alias `--format`.
</ParamField>

<ParamField body="-J / --json" type="flag">
Shorthand for `--output json`. When placed after tool args on `ati run`, clap may swallow it via `trailing_var_arg`; `call.rs` re-detects `-J`/`--json` for output formatting.
</ParamField>

<ParamField body="--verbose" type="flag">
Enables debug tracing (`RUST_LOG` still controls subscriber filters).
</ParamField>

## Runtime layout

ATI state lives under one directory, resolved in order: `ATI_DIR` → `$HOME/.ati` → `.ati`.

:::files
~/.ati/
├── manifests/     # Provider catalog (*.toml); repo ships curated manifests
├── specs/         # OpenAPI JSON referenced by openapi handlers
├── skills/        # Installed SKILL.md methodology docs
├── config.toml    # Proxy/JWT settings (created by `ati init --proxy`)
├── credentials    # Dev-mode plaintext keys (optional)
├── keyring.enc    # AES-256-GCM encrypted keys (local mode)
└── (jwt-*.pem)    # ES256 key pair when `ati init --proxy --es256`
:::

`ati init` explicitly creates `manifests/`, `specs/`, and `skills/`. Many commands also call `ensure_ati_dir()` to lazily create the same tree on first use.

The repository bundles curated manifests (for example `clinicaltrials`, `finnhub`, `github-mcp`, `deepwiki-mcp`, `google-workspace`, `crossref`, `hackernews`) under `manifests/`. User-added providers land in `~/.ati/manifests/` via `ati provider import-openapi`, `add-mcp`, or `add-cli`.

## Execution modes

`ati run` auto-detects mode from the environment — agents keep the same command shape.

```mermaid
flowchart TB
  subgraph cli["ati run (src/cli/call.rs)"]
    RUN[parse_tool_args]
    FM{file_manager tool?}
    PROXY{ATI_PROXY_URL set?}
    LOCAL[execute_local]
    PX[execute_via_proxy → POST /call]
    FMEXEC[execute_file_manager]
  end

  subgraph local["Local mode"]
    REG[ManifestRegistry::load manifests/]
    KR[load_keyring: keyring.enc → credentials → empty]
    SCOPE[load_local_scopes_from_env]
    DISP{provider.handler}
    HTTP[core/http + generic]
    MCP[core/mcp_client]
    CLI[core/cli_executor]
  end

  subgraph proxy["Proxy mode"]
    JWT[ATI_SESSION_TOKEN Bearer]
    PSERVER[ati proxy: keyring + scopes + upstream]
  end

  RUN --> FM
  FM -->|yes| FMEXEC
  FM -->|no| PROXY
  PROXY -->|yes| PX --> PSERVER
  PROXY -->|no| LOCAL --> REG --> KR --> SCOPE --> DISP
  DISP --> HTTP
  DISP --> MCP
  DISP --> CLI
```

| Mode | Trigger | Where credentials live | Typical use |
|------|---------|------------------------|-------------|
| Dev | No `keyring.enc`; `~/.ati/credentials` or `ati key set` | Plaintext `credentials` (0600) | Local development |
| Local | `keyring.enc` + optional `/run/ati/.key` one-shot session key | mlock'd keyring in-process | Sandboxed agents with encrypted keyring |
| Proxy | `ATI_PROXY_URL` set | Proxy host `keyring.enc` / `--env-keys` | Untrusted sandboxes — zero keys in agent VM |

<Note>
In local mode, if JWT validation env vars are configured (`ATI_JWT_PUBLIC_KEY`, `ATI_JWT_SECRET`, etc.), `ATI_SESSION_TOKEN` becomes mandatory and scopes are enforced from claims. Without JWT config, local mode stays unrestricted for development.
</Note>

Proxy client requests use `Authorization: Bearer` from `ATI_SESSION_TOKEN` (with `ATI_SESSION_TOKEN_FILE` and `/run/ati/session_token` fallbacks per `src/proxy/client.rs`).

## Provider catalog and tool naming

Each `[provider]` block in a manifest TOML defines auth, `handler`, and transport-specific fields. Tools are either hand-written `[[tools]]` entries (HTTP) or auto-discovered (OpenAPI `tools/list`, MCP `tools/list`).

| Handler | Discovery | Example tool name |
|---------|-----------|-------------------|
| `http` (default) | `[[tools]]` in manifest | `medical_search` |
| `openapi` | Spec operations at registry load | `finnhub:quote` |
| `mcp` | Live `tools/list` on first use | `github:search_repositories` |
| `cli` | Single wrapper per provider | `gh` (args after `--`) |
| `file_manager` | Built-in virtual provider | `file_manager:download` |
| `passthrough` | Proxy edge reverse-proxy routes | Manifest-defined paths |

MCP and OpenAPI tools use the `provider:tool` separator (`TOOL_SEP` = `:`). Dispatch strips the provider prefix before calling the upstream MCP server.

## Shortest path: init to scoped tool call

<Steps>
<Step title="Install and initialize">

```bash
# Pre-built binary or: cargo install agent-tools-interface
ati init
```

Creates `~/.ati/manifests`, `specs`, `skills`, and a starter `config.toml` (unless you pass `--proxy` to generate JWT material).

</Step>

<Step title="Add a provider">

```bash
# No API key — ClinicalTrials.gov ships in-repo; import refreshes user manifest
ati provider import-openapi https://clinicaltrials.gov/api/v2/openapi.json

# Or use a bundled manifest already under manifests/ after copying to ~/.ati
```

</Step>

<Step title="Store credentials (if required)">

```bash
ati key set finnhub_api_key "your-key"
ati key list   # values masked
```

Skip when `auth_type = "none"`.

</Step>

<Step title="Discover and run">

```bash
ati tool list --provider clinicaltrials
ati tool info clinicaltrials:searchStudies
ati run clinicaltrials:searchStudies --query.term "cancer" --pageSize 3
```

</Step>

<Step title="Optional: scope via JWT (proxy or strict local)">

```bash
ati init --proxy              # or --proxy --es256
ati token issue --sub agent-1 --scope "tool:clinicaltrials:* help" --ttl 3600
export ATI_SESSION_TOKEN="<jwt>"
export ATI_PROXY_URL=http://127.0.0.1:8090   # proxy mode
ati proxy --port 8090 --ati-dir ~/.ati       # on the secrets host

ati run clinicaltrials:searchStudies --query.term "cancer"   # allowed
ati run finnhub:quote --symbol AAPL                          # denied if not in scope
```

</Step>
</Steps>

<Check>
Verify success with `ati tool info <name>` (schema + usage line) or `ati --output json run <name> ...` for structured output.
</Check>

## Tool discovery tiers

Agents typically combine three discovery paths before `ati run`:

1. **`ati tool search <query>`** — Offline fuzzy match across names, descriptions, tags, and hints.
2. **`ati tool info <name>`** — Full input schema, endpoint, handler, and example command.
3. **`ati assist "<question>"`** — LLM context over visible tools (and resolved skills); requires `help` scope in proxy mode when JWT is enforced.

In proxy mode, `ati tool list`, `ati tool search`, and `ati assist` can route to the proxy so listings respect JWT scopes (same as `/tools` on the server).

## Proxy mode contract

Set two variables in the agent sandbox:

| Variable | Purpose |
|----------|---------|
| `ATI_PROXY_URL` | Base URL (e.g. `http://proxy:8090`) — switches `ati run` to proxy client |
| `ATI_SESSION_TOKEN` | Bearer JWT with `scope` claim (`tool:…`, `skill:…`, `help`, `*`) |

The proxy executes `POST /call` with `{ "tool_name", "args" }` (and optional `raw_args` for CLI tools). It loads manifests and keyring from `--ati-dir`, validates JWT when configured, injects auth, and calls upstream HTTP/MCP/CLI.

<Warning>
When JWT validation is configured on the proxy, missing or invalid `ATI_SESSION_TOKEN` is rejected — there is no partial trust. Unconfigured JWT on the proxy keeps dev-style open access.
</Warning>

## Security and credentials (summary)

Local `load_keyring` cascades: sealed `keyring.enc` (session key at `ATI_KEY_FILE`, default `/run/ati/.key`) → persistent local key → plaintext `credentials` → empty keyring. OAuth2 tokens and generated credentials use an in-memory `AuthCache` during the call.

Proxy mode keeps API keys off the agent filesystem entirely; only manifests (no secrets) and the session JWT need to be provisioned into the sandbox.

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Pre-built musl binaries, `cargo install`, Docker, and platform prerequisites.
</Card>
<Card title="Quickstart" href="/quickstart">
End-to-end init, import, key storage, list, run, and verify.
</Card>
<Card title="Execution modes" href="/execution-modes">
Dev vs local keyring vs proxy threat model and auto-detection.
</Card>
<Card title="Providers and handlers" href="/providers-and-handlers">
Handler types, `provider:tool` naming, and dispatch modules.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
`ati token` and scoped sandbox provisioning.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full subcommand tree and global flags.
</Card>
</CardGroup>

---

## 02. Installation

> Pre-built release binaries (musl/static), `cargo install agent-tools-interface`, Docker image, and platform prerequisites for local vs proxy deployments.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/02-installation.md
- Generated: 2026-06-02T03:14:00.528Z

### Source Files

- `README.md`
- `Cargo.toml`
- `deploy/Dockerfile`
- `deny.toml`
- `src/cli/init.rs`

---
title: "Installation"
description: "Pre-built release binaries (musl/static), `cargo install agent-tools-interface`, Docker image, and platform prerequisites for local vs proxy deployments."
---

ATI ships as a single `ati` binary (crate `agent-tools-interface`). Install it on the host where tool calls execute (developer laptop, agent sandbox, or proxy VM), then point it at `~/.ati/` (or `ATI_DIR`) for manifests, keys, and skills. Proxy deployments add a second install on the secrets host running `ati proxy`; sandboxes only need the client binary plus `ATI_PROXY_URL` and `ATI_SESSION_TOKEN`.

## Supported platforms

| Platform | Release target | Notes |
|----------|----------------|-------|
| Linux x86_64 | `x86_64-unknown-linux-musl` | Static musl binary; no glibc dependency |
| Linux ARM64 | `aarch64-unknown-linux-musl` | Cross-compiled in CI with `cross` |
| macOS Intel | `x86_64-apple-darwin` | Dynamic macOS binary |
| macOS Apple Silicon | `aarch64-apple-darwin` | Dynamic macOS binary |
| Windows | — | Not in the release matrix |

<Info>
Crates.io package name is `agent-tools-interface`; the installed executable is `ati`. Current crate version in tree: `0.8.0-rc.7`.
</Info>

## Pre-built binaries (recommended)

GitHub Releases (tag `v*`) publish auditable binaries built by `.github/workflows/release.yml`. Each target ships as a **raw executable** plus a sidecar `.sha256` file (not always a `.tar.gz` archive).

### Default CLI binary

<CodeGroup>
```bash title="Linux x86_64"
curl -fSL "https://github.com/Parcha-ai/ati/releases/latest/download/ati-x86_64-unknown-linux-musl" \
  -o ati
chmod +x ati
sudo mv ati /usr/local/bin/
```

```bash title="Linux ARM64"
curl -fSL "https://github.com/Parcha-ai/ati/releases/latest/download/ati-aarch64-unknown-linux-musl" \
  -o ati
chmod +x ati
sudo mv ati /usr/local/bin/
```

```bash title="macOS Apple Silicon"
curl -fSL "https://github.com/Parcha-ai/ati/releases/latest/download/ati-aarch64-apple-darwin" \
  -o ati
chmod +x ati
sudo mv ati /usr/local/bin/
```

```bash title="macOS Intel"
curl -fSL "https://github.com/Parcha-ai/ati/releases/latest/download/ati-x86_64-apple-darwin" \
  -o ati
chmod +x ati
sudo mv ati /usr/local/bin/
```
</CodeGroup>

Pin a specific release by replacing `latest` with the tag (for example `v0.8.0`).

### Feature-flavored Linux binaries

The release job also builds optional compile-time features with filename suffixes:

| Asset suffix | Cargo features | Typical use |
|--------------|----------------|-------------|
| *(none)* | default | Sandboxes, local dev, minimal size |
| `-sentry` | `sentry` | Production proxy with error reporting |
| `-sentry-otel` | `sentry`, `otel` | Production proxy with errors + OTLP traces/metrics |

Example (Sentry-enabled musl, matches `deploy/Dockerfile`):

```bash
curl -fSL "https://github.com/Parcha-ai/ati/releases/download/v0.7.9/ati-x86_64-unknown-linux-musl-sentry" \
  -o ati
curl -fSL "https://github.com/Parcha-ai/ati/releases/download/v0.7.9/ati-x86_64-unknown-linux-musl-sentry.sha256" \
  -o ati.sha256
echo "$(awk '{print $1}' ati.sha256)  ati" | sha256sum -c -
chmod +x ati
```

<Warning>
SemVer prerelease tags (names containing `-`, e.g. `v0.8.0-rc.7`) are marked **prerelease** on GitHub and are **not** published to crates.io or PyPI. Use the GitHub release asset for RC binaries; stable tags publish to `cargo install` and `pip install ati-client`.
</Warning>

### Verify the binary

<Steps>
<Step title="GitHub attestation (recommended)">

```bash
gh attestation verify ./ati --repo Parcha-ai/ati
```

Failure means the binary was not built by the official CI workflow — do not run it.

</Step>
<Step title="SHA256 checksum">

```bash
curl -fSL "https://github.com/Parcha-ai/ati/releases/latest/download/ati-x86_64-unknown-linux-musl.sha256" -o ati.sha256
echo "$(awk '{print $1}' ati.sha256)  ati" | sha256sum -c -
```

</Step>
<Step title="Embedded dependency audit">

Binaries are built with `cargo-auditable`:

```bash
cargo audit bin ./ati
```

</Step>
<Step title="Runtime smoke test">

```bash
ati version
ati init
ati tool list | head
```

</Step>
</Steps>

## Install from Rust (`cargo install`)

Requires a **stable Rust toolchain** and network access to crates.io.

<CodeGroup>
```bash title="crates.io (stable releases only)"
cargo install agent-tools-interface
```

```bash title="GitHub (any tag/branch)"
cargo install --git https://github.com/Parcha-ai/ati.git
# Pin a tag:
cargo install --git https://github.com/Parcha-ai/ati.git --tag v0.8.0
```

```bash title="Clone and build locally"
git clone https://github.com/Parcha-ai/ati.git && cd ati
cargo build --release
# Binary: target/release/ati
```
</CodeGroup>

Release profile (`Cargo.toml`): `opt-level = "z"`, LTO, `strip = true` for small production binaries.

### Static musl build (sandboxes / minimal Linux)

```bash
# x86_64 Linux (needs musl-tools on Ubuntu/Debian)
cargo build --release --target x86_64-unknown-linux-musl

# aarch64 Linux (CI uses cross)
cargo install cross --locked
cross build --release --target aarch64-unknown-linux-musl
```

Optional features (compile-time only):

| Feature | Enables |
|---------|---------|
| `db` | Postgres persistence (`ATI_DB_URL`, `--migrate`) |
| `otel` | OTLP trace/metric export |
| `sentry` | Sentry error reporting |

```bash
cargo build --release --features db --target x86_64-unknown-linux-musl
cargo build --release --features sentry,otel --target x86_64-unknown-linux-musl
```

## Docker image (proxy server)

`deploy/Dockerfile` builds a **proxy-only** image: Debian slim base, pre-downloaded musl binary, bundled `manifests/` and `specs/`, non-root `ati` user.

```text
  docker build (deploy/Dockerfile)
        │
        ├─ debian:bookworm-slim + ca-certificates + curl
        ├─ Node.js 22 (npx for stdio MCP providers)
        ├─ curl GH release → ati-*-unknown-linux-musl-sentry (+ sha256 verify)
        ├─ COPY manifests/ specs/ → ATI_DIR=/app
        └─ CMD: ati proxy --port 18093 --bind 0.0.0.0
```

<ParamField body="ATI_VERSION" type="string" default="v0.7.9">
Docker build arg selecting the GitHub release tag to download. Bump when cutting a new proxy image.
</ParamField>

<ParamField body="TARGETARCH" type="string">
`arm64` → `aarch64-unknown-linux-musl`; otherwise `x86_64-unknown-linux-musl`.
</ParamField>

Build and run:

```bash
docker build -f deploy/Dockerfile \
  --build-arg ATI_VERSION=v0.7.9 \
  -t ati-proxy:local .

docker run --rm -p 18093:18093 \
  -e ATI_DIR=/app \
  -v "$(pwd)/my-secrets:/app:ro" \
  ati-proxy:local
```

| Image default | Value |
|---------------|-------|
| `ATI_DIR` | `/app` (manifests + specs copied in) |
| Listen | `0.0.0.0:18093` |
| User | `ati` (system account) |
| Healthcheck | `GET http://localhost:18093/health` every 30s |
| MCP stdio | Node.js 22 + npm ecosystem via `npx` |

<Note>
The stock Dockerfile does **not** bake in API keys. Mount or inject credentials via encrypted keyring, `ATI_KEY_*` env vars (`ati proxy --env-keys`), or orchestrator-side secret sync. For Postgres audit/virtual keys, use a `-db` build (`--features db`) — see persistence docs.
</Note>

## Initialize `~/.ati/`

After installing `ati`, create the config tree (also auto-created on first use):

```bash
ati init
```

`ati init` creates under `ATI_DIR` (default `~/.ati/`):

| Path | Purpose |
|------|---------|
| `manifests/` | Provider TOML catalog |
| `specs/` | Downloaded OpenAPI specs |
| `skills/` | Installed SKILL.md trees |
| `config.toml` | Optional proxy/JWT settings |

Proxy-oriented init (writes JWT config and keys):

```bash
ati init --proxy              # HS256 secret in config.toml
ati init --proxy --es256      # ES256 key pair: jwt-private.pem (0600), jwt-public.pem
```

`--es256` requires `--proxy`. With `--proxy`, `config.toml` is **always overwritten**; without it, a default stub is written only if missing.

Override data directory:

```bash
export ATI_DIR=/var/lib/ati
ati init --proxy --es256
```

Resolution order for `ATI_DIR`: `ATI_DIR` env → `$HOME/.ati` → fallback `.ati`.

## Deployment prerequisites

Where you install `ati` depends on execution mode. The same binary serves both roles; environment variables select behavior.

```mermaid
flowchart TB
  subgraph sandbox["Agent sandbox / dev host"]
    A[ati binary]
    M[manifests from ATI_DIR or image]
    A --> M
  end

  subgraph local["Local mode (default)"]
    K[keyring.enc or credentials]
    SK["/run/ati/.key optional"]
    A --> K
    SK -.-> K
    A --> U[Upstream APIs / MCP / CLIs]
  end

  subgraph proxy["Proxy mode"]
    P[ati proxy on secrets host]
    KR[keyring + JWT validation]
    P --> KR
    P --> U
    A -->|ATI_PROXY_URL POST /call| P
    JWT[ATI_SESSION_TOKEN] -.-> A
  end
```

### Local and dev installs (single host)

| Requirement | Dev (plaintext) | Local (encrypted keyring) |
|-------------|-----------------|---------------------------|
| Binary | `ati` on `PATH` | Same |
| Data dir | `~/.ati/` or `ATI_DIR` | Same |
| Credentials | `ati key set` → `credentials` (0600) | `keyring.enc` + session key |
| Session key | — | `/run/ati/.key` (default) or `ATI_KEY_FILE`; one-shot unlink after read |
| Scopes | Optional JWT in env for filtering | Same |
| MCP stdio | `npx` / provider command on `PATH` | Same on host running MCP subprocess |
| MCP HTTP | Outbound HTTPS | Same |

No separate proxy process. Keys stay on the machine running `ati run`.

### Proxy installs (split trust)

| Component | Installs | Needs |
|-----------|----------|-------|
| **Proxy host** | `ati` (often `-sentry` or `-sentry-otel` build) | `ATI_DIR` with `manifests/`, `keyring.enc` or `--env-keys`, JWT keys (`ati init --proxy` or env), outbound network to upstreams |
| **Agent sandbox** | `ati` only (musl static typical) | `ATI_PROXY_URL`, `ATI_SESSION_TOKEN`; **no** keyring or API keys |
| **Optional edge** | Caddy/haproxy in front | TLS, HMAC sig-verify secret in keyring; see VM example |

Proxy CLI defaults (`ati proxy`):

<ParamField body="--port" type="number" default="8090">
Listen port.
</ParamField>

<ParamField body="--bind" type="string" default="127.0.0.1">
Bind address; use `0.0.0.0` for remote sandboxes (Dockerfile uses `0.0.0.0`).
</ParamField>

<ParamField body="--ati-dir" type="string">
Override manifests/keyring directory (Docker/systemd: `/var/lib/ati` or `/app`).
</ParamField>

<ParamField body="--env-keys" type="flag">
Load secrets from `ATI_KEY_*` environment variables instead of `keyring.enc`.
</ParamField>

<ParamField body="--migrate" type="flag">
Apply embedded Postgres migrations when `ATI_DB_URL` is set (`db` feature required).
</ParamField>

Minimal proxy start:

```bash
ati init --proxy --es256
ati key set finnhub_api_key "sk-..."
ati proxy --port 8090 --bind 0.0.0.0 --ati-dir ~/.ati
```

Sandbox client:

```bash
export ATI_PROXY_URL=http://proxy-host:8090
export ATI_SESSION_TOKEN="$(ati token issue --sub agent-1 --scope 'tool:finnhub:* help' --ttl 3600)"
ati run finnhub:quote --symbol AAPL
```

### Optional runtime dependencies by provider type

| Provider handler | Extra host software |
|------------------|---------------------|
| MCP stdio | Command in manifest (commonly `npx` + Node.js) |
| MCP HTTP | Outbound HTTPS only |
| OpenAPI / HTTP | Outbound HTTPS |
| CLI (`ati provider add-cli`) | Wrapped binary on `PATH` (e.g. `gh`, `gcloud`) |
| Skills + LLM assist | LLM API key in keyring for `_llm` internal provider |

The Docker proxy image pre-installs **Node.js 22** specifically for stdio MCP servers launched via `npx`.

### Production proxy checklist

- Set `ATI_JWT_PUBLIC_KEY` / `ATI_JWT_SECRET` (or config.toml `[proxy.jwt]`) so anonymous access is rejected.
- Set `ATI_SSRF_PROTECTION=1` and `ATI_DOWNLOAD_ALLOWLIST` when using `file_manager:download`.
- Prefer `-sentry` / `-sentry-otel` release artifacts or image tags for observability.
- For edge VMs: copy `deploy/examples/vm/` systemd/Caddy templates; bootstrap keyring with `ati edge bootstrap-keyring` when using 1Password.
- Run `cargo deny check` in CI mirrors supply-chain policy (`deny.toml`: no unknown registries, no git deps, license allowlist).

## Troubleshooting

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `cargo install` resolves old version | Installed during an RC period; crates.io skips prerelease tags | Install from GitHub release asset or `--git --tag` |
| `ati proxy` missing `--migrate` | Binary built without `db` feature | Rebuild with `--features db` or use `-db` release variant |
| MCP stdio spawn fails in container | Node/`npx` missing | Use stock Dockerfile or install Node 22 on host |
| Proxy rejects all calls | JWT enforced, token missing/invalid | `ati token issue` + export `ATI_SESSION_TOKEN` |
| Keyring not loaded in proxy | Wrong `--ati-dir` | Manifests and `keyring.enc` must live under same `ATI_DIR` |
| musl aarch64 build fails locally | Needs cross toolchain | `cargo install cross && cross build ...` |

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
Initialize `~/.ati/`, import a provider, store a key, and run your first `ati run`.
</Card>
<Card title="Execution modes" href="/execution-modes">
Dev, local encrypted keyring, and proxy mode — credential placement and threat model.
</Card>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
`ati proxy` flags, systemd/VM layout, passthrough, and health probes.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
`ati init --proxy`, token issuance, and keyring management.
</Card>
<Card title="Build, test, and troubleshooting" href="/build-test-and-troubleshooting">
`cargo test`, musl targets, feature flags, and E2E scripts.
</Card>
</CardGroup>

---

## 03. Quickstart

> Initialize `~/.ati/`, import a no-auth OpenAPI provider, store a key, run `ati tool list` and `ati run`, and verify success with `ati tool info`.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/03-quickstart.md
- Generated: 2026-06-02T03:14:16.103Z

### Source Files

- `README.md`
- `src/cli/init.rs`
- `src/cli/call.rs`
- `manifests/clinicaltrials.toml`
- `specs/clinicaltrials.json`
- `src/cli/tools.rs`

---
title: "Quickstart"
description: "Initialize `~/.ati/`, import a no-auth OpenAPI provider, store a key, run `ati tool list` and `ati run`, and verify success with `ati tool info`."
---

Local mode loads provider manifests from `~/.ati/manifests/`, resolves OpenAPI tools from `~/.ati/specs/`, and executes HTTP calls through `ati run` without proxy credentials when `ATI_PROXY_URL` is unset.

## Prerequisites

Install the `ati` binary (release tarball, `cargo install agent-tools-interface`, or a local `cargo build --release`). You need outbound HTTPS to fetch an OpenAPI spec URL.

<CardGroup>
  <Card title="Installation" href="/installation">
    Pre-built musl binaries, crates.io install, and platform notes.
  </Card>
</CardGroup>

## What gets created

`ati init` and first-use auto-setup both target the same directory. Resolution order is `ATI_DIR` → `$HOME/.ati` → `.ati`.

| Path | Role |
|------|------|
| `~/.ati/manifests/` | Provider TOML files (`[provider]`, optional `[[tools]]`) |
| `~/.ati/specs/` | OpenAPI JSON referenced by `openapi_spec` |
| `~/.ati/skills/` | Installed skill directories |
| `~/.ati/config.toml` | Optional proxy/JWT settings |
| `~/.ati/credentials` | Dev-mode plaintext keys (`ati key set`) |

<Note>
Every command calls `ensure_ati_dir()` before running, which creates `manifests/`, `specs/`, `skills/`, and a stub `config.toml` if missing. `ati init` is still the explicit way to lay out the tree and print next-step hints.
</Note>

:::files
~/.ati/
├── config.toml
├── manifests/
│   └── clinicaltrials.toml    # after import-openapi
├── specs/
│   └── clinicaltrials.json    # downloaded/normalized spec
├── skills/
└── credentials                # after ati key set (optional for no-auth)
:::

## End-to-end flow

```mermaid
sequenceDiagram
  participant You
  participant CLI as ati CLI
  participant Dir as ~/.ati/
  participant API as clinicaltrials.gov

  You->>CLI: ati init
  CLI->>Dir: manifests/, specs/, skills/, config.toml
  You->>CLI: ati provider import-openapi URL
  CLI->>API: GET openapi.json
  API-->>CLI: OpenAPI 3.0 spec
  CLI->>Dir: specs/clinicaltrials.json
  CLI->>Dir: manifests/clinicaltrials.toml
  You->>CLI: ati tool list --provider clinicaltrials
  CLI->>Dir: load manifest + register OpenAPI tools
  You->>CLI: ati run clinicaltrials:searchStudies ...
  CLI->>API: GET /studies
  API-->>CLI: JSON studies
  You->>CLI: ati tool info clinicaltrials:searchStudies
  CLI-->>You: schema, endpoint, usage line
```

<Steps>
  <Step title="Initialize the ATI directory">
    ```bash
    ati init
    ```

    Creates `manifests/`, `specs/`, and `skills/` under `~/.ati/`, and writes `config.toml` when it does not exist. Use `ati init --proxy` only when you are preparing a JWT-backed proxy host (not required for this local quickstart).

    Verify:

    ```bash
    ls ~/.ati/manifests ~/.ati/specs ~/.ati/skills
    ```
  </Step>

  <Step title="Import a no-auth OpenAPI provider">
    ClinicalTrials.gov publishes a public OpenAPI spec with no API key. ATI derives the provider name `clinicaltrials` from the host `clinicaltrials.gov`.

    ```bash
    ati provider import-openapi https://clinicaltrials.gov/api/v2/openapi.json
    ```

    The command downloads the spec (30s timeout, SSRF checks on the URL), detects `auth_type = "none"`, writes `~/.ati/specs/clinicaltrials.json`, and generates `~/.ati/manifests/clinicaltrials.toml` with `handler = "openapi"`. OpenAPI operations register at load time as tools named `clinicaltrials:<operationId>` (for example `clinicaltrials:searchStudies`).

    Preview without writing files:

    ```bash
    ati provider import-openapi https://clinicaltrials.gov/api/v2/openapi.json --dry-run
    ```

    Override the derived name:

    ```bash
    ati provider import-openapi https://clinicaltrials.gov/api/v2/openapi.json --name clinicaltrials
    ```
  </Step>

  <Step title="Store a credential (dev mode)">
    No-auth providers do not read a keyring entry on `ati run`, but storing a key exercises the dev credential path used by authenticated providers.

    ```bash
    ati key set finnhub_api_key "your-key-here"
    ati key list
    ```

    Keys persist in `~/.ati/credentials` as JSON with file mode `0600`. `ati key list` prints masked values. When you later import an API that sets `auth_key_name` in its manifest, ATI resolves that name from the same store (or from `keyring.enc` in local/proxy hardened setups).

    <Tip>
    For the ClinicalTrials.gov step alone, skip `ati key set` — `auth_type = "none"` omits `auth_key_name` from the generated manifest.
    </Tip>
  </Step>

  <Step title="List tools">
    ```bash
    ati tool list --provider clinicaltrials
    ```

    Registry load reads `~/.ati/manifests/*.toml`, loads the spec from `~/.ati/specs/`, and registers each OpenAPI operation. Table output is the default when you pass `--output table`; the CLI global default is `json` unless you set `ATI_OUTPUT`.

    ```bash
    ATI_OUTPUT=table ati tool list --provider clinicaltrials | head -5
    ```

    Expect rows with columns `PROVIDER`, `TOOL`, and `DESCRIPTION`, where `TOOL` values look like `clinicaltrials:searchStudies`.
  </Step>

  <Step title="Run a tool">
    Query parameters from the spec are passed as `--query.term`, `--pageSize`, and similar flags. ATI classifies them using `x-ati-param-location` metadata injected during OpenAPI import.

    ```bash
    ati --output text run clinicaltrials:searchStudies --query.term "cancer" --pageSize 3
    ```

    With `ATI_PROXY_URL` unset, `ati run` loads manifests and credentials locally, classifies arguments, and issues `GET https://clinicaltrials.gov/api/v2/studies` with the query string built from your flags.

    <RequestExample>
    ```bash
    ati --output json run clinicaltrials:searchStudies --query.term "cancer" --pageSize 3
    ```
    </RequestExample>

    <ResponseExample>
    ```json
    {
      "studies": [ "... trial records ..." ],
      "nextPageToken": "..."
    }
    ```
    </ResponseExample>
  </Step>

  <Step title="Verify with tool info">
    ```bash
    ati --output text tool info clinicaltrials:searchStudies
    ```

    Confirms the namespaced tool name, provider description, `handler: openapi`, resolved HTTP endpoint, input schema properties (`query.term`, `pageSize`, filters), and a generated usage line.

    ```bash
    ati --output json tool info clinicaltrials:searchStudies
    ```

    Returns structured fields: `name`, `provider`, `method`, `endpoint`, `input_schema`, `skills`.
  </Step>
</Steps>

## Command reference (this walkthrough)

| Command | Purpose |
|---------|---------|
| `ati init` | Create `~/.ati/` layout and default `config.toml` |
| `ati provider import-openapi <url>` | Download spec, write manifest + spec, register tools |
| `ati key set <name> <value>` | Store dev credential in `~/.ati/credentials` |
| `ati key list` | List key names with masked values |
| `ati tool list [--provider NAME]` | Scope-filtered catalog of public tools |
| `ati run <provider>:<tool> --arg value` | Execute one tool call |
| `ati tool info <provider>:<tool>` | Schema, endpoint, and example usage |

## Generated manifest shape

After import, `~/.ati/manifests/clinicaltrials.toml` matches the curated repo manifest: OpenAPI handler, no auth, spec file reference.

```toml
[provider]
name = "clinicaltrials"
description = "ClinicalTrials.gov — Search the US clinical trials database"
handler = "openapi"
base_url = "https://clinicaltrials.gov/api/v2"
openapi_spec = "clinicaltrials.json"
auth_type = "none"
category = "medical"
```

Tools are not listed in TOML; they are materialized from `clinicaltrials.json` at registry load time.

## Troubleshooting

| Symptom | Likely cause | What to do |
|---------|----------------|------------|
| `no tools available` on `ati tool list` | Empty `~/.ati/manifests/` or scope restriction | Run `import-openapi` or check `ATI_SESSION_TOKEN` scopes when JWT validation is enabled locally |
| `Unknown tool: 'clinicaltrials:searchStudies'` | Manifest/spec missing or load error | Confirm `~/.ati/manifests/clinicaltrials.toml` and `~/.ati/specs/clinicaltrials.json` exist; re-run import |
| Import URL fails with SSRF message | Private/redirect URL blocked | Use the public HTTPS spec URL directly |
| `ATI_SESSION_TOKEN is required` | JWT validation configured without a token | Unset JWT env vars for unrestricted dev mode, or issue a token with `ati token issue` |
| Empty or error HTTP response on `ati run` | Network or upstream API issue | Retry with `--verbose`; confirm parameters via `ati tool info` |

<Warning>
When `ATI_JWT_PUBLIC_KEY`, `ATI_JWT_SECRET`, or proxy JWT settings in `config.toml` are active, local commands require a valid `ATI_SESSION_TOKEN`. Without JWT configuration, local mode stays unrestricted for development.
</Warning>

## Related pages

<CardGroup>
  <Card title="Overview" href="/overview">
    Execution surfaces, provider catalog, and proxy vs local routing.
  </Card>
  <Card title="Import OpenAPI providers" href="/import-openapi">
    Tag filters, operation caps, `inspect-openapi`, and auth detection.
  </Card>
  <Card title="Execution modes" href="/execution-modes">
    Dev credentials, encrypted keyring, and `ATI_PROXY_URL` proxy mode.
  </Card>
  <Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
    `ati key`, `ati token`, and scoped session tokens.
  </Card>
  <Card title="Scopes and tool discovery" href="/scopes-and-tool-discovery">
    `ati tool search`, scope-filtered listing, and `ati assist`.
  </Card>
  <Card title="OpenAPI stock research workflow" href="/openapi-stock-research">
    Finnhub import, key setup, and chained quote calls.
  </Card>
</CardGroup>

---

## 04. Execution modes

> Dev (plaintext credentials), local (AES-256-GCM keyring + one-shot session key), and proxy (`ATI_PROXY_URL`) modes: credential placement, auto-detection in `ati run`, and threat-model trade-offs.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/04-execution-modes.md
- Generated: 2026-06-02T03:14:09.957Z

### Source Files

- `docs/SECURITY.md`
- `src/cli/call.rs`
- `src/proxy/client.rs`
- `src/core/keyring.rs`
- `src/security/memory.rs`
- `src/security/sealed_file.rs`

---
title: "Execution modes"
description: "Dev (plaintext credentials), local (AES-256-GCM keyring + one-shot session key), and proxy (`ATI_PROXY_URL`) modes: credential placement, auto-detection in `ati run`, and threat-model trade-offs."
---

`ati run` routes every tool call through one of two runtime paths: **local** (decrypt credentials in-process and call upstream APIs directly) or **proxy** (forward `POST /call` to an external `ati proxy` that holds keys). Credential storage is a separate axis—plaintext `~/.ati/credentials`, encrypted `keyring.enc`, or no keys in the sandbox at all. The same CLI command and manifest layout work in all combinations; only environment variables and on-disk artifacts change.

## Mode matrix

| Dimension | Dev credentials | Local (sandbox) | Proxy (`ATI_PROXY_URL`) |
|-----------|-----------------|-----------------|-------------------------|
| **Where API keys live** | `~/.ati/credentials` (JSON, mode `0600`) or `ATI_KEY_*` env on the proxy | `~/.ati/keyring.enc` + session key, or same `credentials` fallback | Proxy host only (`keyring.enc`, `credentials`, or `ATI_KEY_*` via `--env-keys`) |
| **Session decrypt key** | N/A (plaintext) | One-shot `/run/ati/.key` (deleted after read) or persistent `~/.ati/.keyring-key` | Not in agent sandbox |
| **Who calls upstream** | Local `ati` process | Local `ati` process | Proxy server |
| **Typical use** | Laptop setup, `ati key set` | Orchestrator-provisioned sandboxes | Untrusted / multi-tenant sandboxes |
| **Scope enforcement** | Unrestricted if JWT keys unset locally; strict if `ATI_JWT_*` configured | Same as dev for local CLI | JWT on proxy when configured; dev passthrough if not |

<Note>
**Dev** names two related behaviors: (1) plaintext credential files via `ati key set`, and (2) **unrestricted scopes** when JWT validation is not configured (`ScopeConfig::unrestricted()`). Production sandboxes should use encrypted keyrings or proxy mode plus JWT issuance—not plaintext files with wildcard scopes.
</Note>

## Auto-detection in `ati run`

Dispatch is env-driven. Agents do not pass a `--mode` flag.

```mermaid
sequenceDiagram
    participant Agent
    participant ATI as ati (cli/call.rs)
    participant Keyring as load_keyring
    participant Upstream as HTTP / MCP / CLI
    participant Proxy as ati proxy

    Agent->>ATI: ati run tool_name [--arg val]
    alt file_manager:* tool
        ATI->>ATI: execute_file_manager (local or proxy branch)
    else ATI_PROXY_URL set
        ATI->>Proxy: POST /call + Bearer JWT
        Proxy->>Upstream: inject auth, enforce scopes
        Proxy-->>ATI: { result, error }
    else local path
        ATI->>Keyring: cascade keyring.enc → credentials → empty
        ATI->>Upstream: generic / mcp / cli handlers
    end
    ATI-->>Agent: formatted stdout
```

| Check | Result |
|-------|--------|
| Tool name is `file_manager:download` or `file_manager:upload` | `execute_file_manager` — client-side file I/O, then local keyring or proxy `/call` |
| `ATI_PROXY_URL` is set (any non-empty value) | `execute_via_proxy` → `proxy/client.rs` `POST {url}/call` |
| Otherwise | `execute_local` → manifests from `ATI_DIR` or `~/.ati/manifests`, keyring cascade, direct upstream |

`file_manager` tools follow the same proxy/local split: if `ATI_PROXY_URL` is set, payload goes to the proxy; otherwise the local keyring is loaded.

## Dev credentials (plaintext)

Development stores secrets in a JSON object at `{ATI_DIR}/credentials` (default `~/.ati/credentials`).

<Steps>
<Step title="Store a key">
```bash
ati key set github_token ghp_xxxxxxxx
ati key list    # values masked in output
```
`ati key set` writes pretty-printed JSON and sets Unix permissions to `0600`.
</Step>
<Step title="Run a tool">
```bash
ati run github:search_repositories --query "ati"
```
With no `ATI_PROXY_URL` and no decryptable `keyring.enc`, `load_keyring` falls through to `Keyring::load_credentials`.
</Step>
</Steps>

Additional dev inputs:

| Mechanism | Behavior |
|-----------|----------|
| `@file:/path/to/secret` in `credentials` | Value replaced with trimmed file contents at load time (Kubernetes/Docker secret mounts) |
| `ATI_KEY_<NAME>` env vars | Proxy startup with `ati proxy --env-keys` scans env, lowercases the suffix into keyring names (e.g. `ATI_KEY_FINNHUB_API_KEY` → `finnhub_api_key`) |

Plaintext credentials never use `mlock` or the sealed-key path; they are appropriate only where disk confidentiality matches your threat model.

## Local mode (encrypted keyring)

Production sandboxes provision an **AES-256-GCM** blob plus a **one-shot session key**. Format: `[12-byte nonce][ciphertext + 16-byte GCM tag]`. Rust uses the `aes-gcm` crate; decrypted JSON becomes a `HashMap<String, String>` in a `Keyring` struct.

### Key delivery lifecycle

```text
Orchestrator                         Sandbox (ATI_DIR)
────────────                         ─────────────────
random 256-bit session key    →      /run/ati/.key  (tmpfs, mode 0400)
encrypt keys → keyring.enc    →      ~/.ati/keyring.enc (or ATI_DIR)
manifests/*.toml              →      ~/.ati/manifests/

First ati invocation:
  1. open /run/ati/.key → unlink path → read fd (TOCTOU-safe)
  2. decrypt keyring.enc
  3. mlock + MADV_DONTDUMP on decrypted buffer
  4. session key zeroed; keys only in locked heap until Drop
```

<ParamField body="ATI_KEY_FILE" type="string">
Overrides the default session key path `/run/ati/.key` (tests and custom supervisors).
</ParamField>

### Persistent keyring (proxy host / long-lived VM)

When `/run/ati/.key` is absent, `Keyring::load_local` reads `~/.ati/.keyring-key` (base64-encoded 32-byte key), decrypts `keyring.enc`, and sets `ephemeral: false`. The key file is **not** deleted—used by `ati proxy` and edge deployments that SIGHUP-reload the keyring.

### Memory protections

On Linux, after decryption:

- `mlock()` — best-effort; warns if `RLIMIT_MEMLOCK` blocks swap exclusion
- `madvise(MADV_DONTDUMP)` — exclude from core dumps
- `Zeroize` on `Drop` for all key strings and raw JSON bytes
- `munlock()` using lengths captured before zeroization

Non-Linux builds log and continue without failing the call.

### Local keyring load cascade

`load_keyring(ati_dir)` in `cli/call.rs` tries, in order:

1. `keyring.enc` + sealed key from `read_and_delete_key()` (`security/sealed_file.rs`)
2. `keyring.enc` + `ati_dir/.keyring-key` (persistent)
3. `credentials` plaintext JSON
4. `Keyring::empty()` — auth-required tools fail until keys are provisioned

Tracing labels these paths as `keyring.enc (sealed key)`, `keyring.enc (persistent key)`, `credentials (plaintext)`, or empty.

## Proxy mode (`ATI_PROXY_URL`)

Set `ATI_PROXY_URL` (for example `http://proxy-host:8090`) in the sandbox. The agent process holds **no** `keyring.enc`, session key, or API secrets—only manifests, optional scope metadata, and the proxy base URL.

### Sandbox → proxy request

| Field | Source |
|-------|--------|
| URL | `POST {ATI_PROXY_URL}/call` |
| Body | `{ "tool_name", "args", "raw_args"? }` — `raw_args` preserves CLI positional tokens |
| `Authorization` | `Bearer` JWT from `ATI_SESSION_TOKEN`, `<NAME>_FILE`, or default file `/run/ati/session_token` |
| `X-Ati-Upstream-Url` | Optional per-manifest `mcp_url_env` override (proxy validates against allowlist) |

Per-provider audience separation: if the manifest sets `auth_session_token_env` (for example `PARCHA_TOOLS_SESSION_TOKEN`), the client reads that env (with the same file fallback chain) and falls back to `ATI_SESSION_TOKEN` if unset—avoiding silent unauthenticated requests.

Proxy timeout: **120 seconds** per HTTP client (`PROXY_TIMEOUT_SECS`).

### What stays out of the sandbox

| Present | Absent |
|---------|--------|
| `ati` binary, `manifests/*.toml` | `keyring.enc` |
| `ATI_PROXY_URL`, `ATI_SESSION_TOKEN` (or file) | `/run/ati/.key` |
| Scope-filtered tool visibility via JWT | Raw API keys |

<Warning>
`ATI_PROXY_URL` is visible in the environment—it is not a secret. Security relies on JWT scope validation (when enabled), network placement, and TLS to the proxy—not hiding the URL.
</Warning>

### Proxy server credential loading

`ati proxy` uses the same cascade as local CLI, plus:

| Flag / env | Effect |
|------------|--------|
| `--ati-dir PATH` | Manifests and key material root (default `ATI_DIR` / `~/.ati`) |
| `--env-keys` | `Keyring::from_env()` only—no disk keyring |
| SIGHUP | `reload_keyring` hot-swaps decrypted keys and sig-verify secrets |

Startup logs `keyring.enc (sealed key)`, `keyring.enc (persistent key)`, `credentials (plaintext)`, or `env-vars (ATI_KEY_*)`.

## Scopes and JWT across modes

Scope behavior splits on whether JWT validation is configured—not on proxy vs local alone.

| JWT config | Local CLI (`load_local_scopes_from_env`) | Proxy (`auth_middleware`) |
|------------|------------------------------------------|---------------------------|
| **Not configured** | Unrestricted dev scopes | Unauthenticated requests allowed (unless `Ati-Key` header present) |
| **Configured** (`ATI_JWT_PUBLIC_KEY` or `ATI_JWT_SECRET`) | Requires valid `ATI_SESSION_TOKEN`; scopes from JWT `scope` claim | Requires `Authorization: Bearer`; 401 if missing/invalid |

When JWT is enabled, enforcement applies consistently to `ati run`, `POST /call`, `POST /mcp`, `/tools`, `/skills`, `/help`, and SkillATI routes. Discovery endpoints are scope-filtered so agents cannot enumerate tools outside their grant.

## Threat-model trade-offs

| Attack / concern | Dev (plaintext) | Local (encrypted) | Proxy |
|------------------|-----------------|-------------------|-------|
| `cat ~/.ati/credentials` | **Exposes secrets** | N/A if only keyring used | N/A in sandbox |
| `cat ~/.ati/keyring.enc` | N/A | Ciphertext only; session key gone after first read | N/A in sandbox |
| `cat /run/ati/.key` | N/A | File unlinked on read; brief race window | N/A |
| Secrets in `env` / `printenv` | Possible if operator exports keys | ATI avoids putting API keys in env | Only proxy URL + JWT in sandbox |
| `ptrace` / memory dump | Full process access if sandbox allows | Mitigated: mlock, DONTDUMP, seccomp in hardened sandboxes; not cryptographic isolation | No keys in agent process |
| Proxy compromise | N/A | N/A | Central credential store—same class as Vault |
| Network MITM to proxy | N/A | N/A | Use HTTPS on `ATI_PROXY_URL` |
| Operational complexity | Lowest | Medium (orchestrator must provision key + blob) | Highest (run proxy, issue JWTs, monitor health) |

<Info>
Honest local-mode limitations (from `docs/SECURITY.md`): without seccomp, `ptrace` can read ATI memory during execution; `mlock` may silently fail on some kernels; orchestrator mis-provisioning leaves tools failing with empty keyring rather than leaking keys.
</Info>

## Choosing a configuration

| Scenario | Recommendation |
|----------|----------------|
| Local laptop, quick provider tests | Dev `credentials` + no JWT |
| Standard agent sandbox (trusted VM) | Local encrypted keyring + JWT scopes |
| Regulated / multi-tenant / untrusted code | Proxy mode + JWT + TLS |
| Air-gapped sandbox | Local (no proxy dependency) |
| Network-restricted sandbox, GCS skills | `ATI_SKILL_REGISTRY=proxy` with `ATI_PROXY_URL` (skills via `/skillati/*`, zero GCS creds in sandbox) |

Switching runtime path is one variable—commands stay identical:

```bash
# Local
ati run finnhub:quote --symbol AAPL

# Proxy (same invocation)
export ATI_PROXY_URL=http://127.0.0.1:8090
export ATI_SESSION_TOKEN="$(ati token issue --sub agent-1 --scope 'tool:finnhub:*' --expires 3h)"
ati run finnhub:quote --symbol AAPL
```

## Verification signals

| Signal | Healthy state |
|--------|---------------|
| `RUST_LOG=debug ati run ...` | `mode: local` or `mode: proxy` plus keyring source line |
| Local sealed provisioning | `/run/ati/.key` absent after first `ati run`; tool auth succeeds |
| Proxy sandbox | `env \| grep ATI_PROXY_URL` set; no `keyring.enc` in sandbox tree |
| JWT enabled locally | Missing `ATI_SESSION_TOKEN` → clear error requiring token |
| Proxy health | `curl $ATI_PROXY_URL/health` reports tool counts and JWT status |

## Related pages

<CardGroup>
<Card title="Overview" href="/overview">
Runtime surfaces (`ati run`, catalog, proxy) and the shortest init-to-call path.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
`ati key`, `ati token issue`, `ati init --proxy`, and session token file rotation.
</Card>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
`ati proxy` flags, binding, `--env-keys`, persistence, and production systemd patterns.
</Card>
<Card title="Environment variables" href="/environment-variables">
`ATI_PROXY_URL`, `ATI_SESSION_TOKEN`, `ATI_DIR`, `ATI_KEY_FILE`, and JWT-related env contract.
</Card>
<Card title="Security and production" href="/security-and-production">
Hardening checklist, SSRF, sig-verify, audit, and edge keyring rotation.
</Card>
<Card title="Agent harness sandbox" href="/agent-harness-sandbox">
Orchestrator provisioning of scoped env, keyring blobs, and proxy-mode agent loops.
</Card>
</CardGroup>

---

## 05. Providers and handlers

> Handler types (`http`, `openapi`, `mcp`, `cli`, `file_manager`, `passthrough`), tool naming (`provider:tool`), auth injection, and how manifests map to dispatch in `core/http`, `mcp_client`, and `cli_executor`.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/05-providers-and-handlers.md
- Generated: 2026-06-02T03:15:14.114Z

### Source Files

- `src/core/manifest.rs`
- `src/core/http.rs`
- `src/core/openapi.rs`
- `src/core/mcp_client.rs`
- `src/core/cli_executor.rs`
- `manifests/finnhub.toml`
- `manifests/github-mcp.toml`
- `manifests/google-workspace.toml`

---
title: "Providers and handlers"
description: "Handler types (`http`, `openapi`, `mcp`, `cli`, `file_manager`, `passthrough`), tool naming (`provider:tool`), auth injection, and how manifests map to dispatch in `core/http`, `mcp_client`, and `cli_executor`."
---

Each TOML manifest under `~/.ati/manifests/` declares one `[provider]` and optional `[[tools]]`. The `handler` field (default `http`) selects the execution backend. `ManifestRegistry` indexes tools by exact name; `ati run <tool>` and proxy `POST /call` resolve the tool, check JWT scopes, then branch on `provider.handler` into `core/http`, `core/mcp_client`, `core/cli_executor`, `core/file_manager`, or (proxy-only) raw HTTP passthrough.

## Handler catalog

Six handler values are accepted at load time and in the provider store:

| Handler | Manifest tools | Execution module | Invoked via |
|---------|----------------|------------------|-------------|
| `http` (default) | Hand-written `[[tools]]` | `core/http` → `providers/generic` | `ati run`, `POST /call` |
| `openapi` | Auto-generated from spec at load | Same HTTP path as `http` | `ati run`, `POST /call` |
| `mcp` | Discovered via MCP `tools/list` | `core/mcp_client` | `ati run`, `POST /call`, `POST /mcp` |
| `cli` | One implicit tool per provider (provider name) if `[[tools]]` empty | `core/cli_executor` | `ati run`, `POST /call` |
| `file_manager` | Built-in `file_manager:download` / `file_manager:upload` | `core/file_manager` (proxy); CLI wrapper in `cli/file_manager` | `ati run` (short-circuited before handler match) |
| `passthrough` | No tools — HTTP routes only | `core/passthrough` | Proxy fallback (not `ati run`) |

<Note>
`openapi` is a load-time specialization: specs are parsed in `core/openapi`, tools are registered into the manifest, and runtime dispatch uses the same HTTP executor as hand-written providers. The manifest still stores `handler = "openapi"` for filtering and discovery.
</Note>

## Dispatch flow

```mermaid
flowchart TB
  subgraph input [Entry]
    RUN["ati run tool_name"]
    CALL["POST /call"]
  end

  subgraph registry [ManifestRegistry]
    LOAD["load manifests/*.toml"]
    OAI["openapi: load_and_register"]
    MCPD["discover_all_mcp_tools"]
    FM["register_file_manager_provider"]
    IDX["tool_index HashMap"]
  end

  subgraph gate [Pre-dispatch]
    FMCHK{"file_manager:* ?"}
    PROXY{"ATI_PROXY_URL set?"}
    SCOPE["scope + rate checks"]
  end

  subgraph exec [Handler branch]
    MCP["mcp → mcp_client::execute_with_gen"]
    CLI["cli → cli_executor::execute_with_gen"]
    FMP["file_manager → dispatch_file_manager"]
    HTTP["http / openapi → http::execute_tool_with_gen"]
    PT["passthrough → handle_passthrough fallback"]
  end

  RUN --> FMCHK
  CALL --> SCOPE
  FMCHK -->|yes| FML["cli/file_manager execute"]
  FMCHK -->|no| PROXY
  PROXY -->|yes| CALL
  PROXY -->|no| LOAD
  LOAD --> OAI --> IDX
  LOAD --> MCPD --> IDX
  LOAD --> FM --> IDX
  RUN --> LOAD
  RUN --> SCOPE
  SCOPE --> MCP
  SCOPE --> CLI
  SCOPE --> FMP
  SCOPE --> HTTP
  CALL --> MCP
  CALL --> CLI
  CALL --> FMP
  CALL --> HTTP
  PT -.->|proxy only, not /call| CALL
```

Local mode (`cli/call.rs`) matches `mcp`, `cli`, and a default arm for everything else (including `http` and `openapi`). Proxy mode (`proxy/server.rs`) adds an explicit `file_manager` arm before the HTTP default.

## Tool naming

The registry key is the tool’s `name` field exactly as indexed — there is no automatic prefixing for hand-written HTTP tools.

| Source | Naming pattern | Example |
|--------|----------------|---------|
| Hand-written HTTP | Flat name (often `{provider}_{action}`) | `hackernews_top_stories` |
| OpenAPI | `{provider}:{operationId}` | `finnhub:quote` |
| MCP (discovered) | `{provider}:{mcp_tool_name}` | `github:search_repositories` |
| CLI (auto tool) | Same as `provider.name` | `google_workspace` |
| File manager (built-in) | Fixed | `file_manager:download`, `file_manager:upload` |

Colon is the canonical separator (`TOOL_SEP` in `core/manifest.rs`). MCP execution strips the provider prefix before `tools/call`:

```text
ati run github:read_file  →  MCP tools/call name "read_file"
```

Scopes are auto-assigned at load as `tool:{tool.name}` when a tool has no explicit `scope` and the provider is not `internal = true`. Wildcard JWT scopes like `tool:github:*` match prefixed MCP/OpenAPI names via scope alias logic in `core/scope.rs`.

<Warning>
Calling `ati run finnhub:quote` requires that exact indexed name. Hand-written tools such as `hackernews_top_stories` are not namespaced with `hackernews:` — check `ati tool info` before scripting calls.
</Warning>

## `http` — hand-written REST tools

Default when `handler` is omitted. Manifests declare `base_url`, `auth_*`, and one or more `[[tools]]` with `endpoint`, `method`, and `input_schema`.

**Parameter routing** (`core/http.rs`):

- **Location-aware** (OpenAPI-generated and any tool with `x-ati-param-location` on schema properties): `path` → URL template substitution; `query` / `header` / `body` routed accordingly; optional `x-ati-body-encoding = "form"`.
- **Legacy** (no location metadata): GET sends all args as query params; POST/PUT/DELETE send JSON body.

**Response shaping**: `providers/generic.rs` calls `http::execute_tool_with_gen`, then `response::process_response` for optional JSONPath `extract` / format from `[tools.response]`.

Example (`manifests/hackernews.toml`): provider `hackernews`, tools `hackernews_top_stories`, `auth_type = "none"`, GET endpoints under Firebase API `base_url`.

## `openapi` — spec-driven HTTP tools

Set `handler = "openapi"`, `openapi_spec` (file under `~/.ati/specs/`), and optional filters (`openapi_include_tags`, `openapi_exclude_tags`, `openapi_include_operations`, `openapi_exclude_operations`, `openapi_max_operations`).

At `ManifestRegistry::load`, `core/openapi::load_and_register` parses the spec and replaces `manifest.tools` with generated tools named `{provider}:{operationId}`. Each property gets `x-ati-param-location` for HTTP classification. PATCH operations map to PUT in ATI’s `HttpMethod` enum.

Example (`manifests/finnhub.toml`):

```toml
[provider]
name = "finnhub"
handler = "openapi"
base_url = "https://finnhub.io/api/v1"
openapi_spec = "finnhub.json"
auth_type = "query"
auth_query_name = "token"
auth_key_name = "finnhub_api_key"
openapi_max_operations = 50
```

Runtime execution is identical to `http` (default `_` branch → `generic::execute_with_gen`).

## `mcp` — MCP protocol providers

No `[[tools]]` required. Tools appear after discovery (`tools/list`), registered with the `provider:` prefix.

| Field | Purpose |
|-------|---------|
| `mcp_transport` | `stdio` (default) or `http` |
| `mcp_command` / `mcp_args` | Subprocess for stdio (e.g. `npx -y @modelcontextprotocol/server-github`) |
| `mcp_url` | Streamable HTTP endpoint |
| `mcp_url_env` | Sandbox env var → proxy validates `X-Ati-Upstream-Url` against keyring allowlist (HTTP only) |
| `mcp_env` | Env map with `${keyring_key}` substitution for stdio |

**Discovery timing**:

- Proxy startup and `ati tool list`: `discover_all_mcp_tools` (30s timeout per provider, concurrent).
- First `ati run` with unknown tool: if name prefix matches an MCP provider, discover that provider once and register tools.

**Auth**: Often `auth_type = "none"` with secrets in `mcp_env` (GitHub token). HTTP MCP may use bearer from keyring or `auth_generator`. `auth_type = "url"` resolves `${key}` inside `mcp_url` at connect time.

Examples:

- `manifests/github-mcp.toml` — stdio, `GITHUB_PERSONAL_ACCESS_TOKEN = "${github_token}"`
- `manifests/deepwiki-mcp.toml` — HTTP, `mcp_url = "https://mcp.deepwiki.com/mcp"`, no auth

## `cli` — subprocess providers

Wraps a host binary (`cli_command`) with curated env (`PATH`, `HOME`, …) plus resolved `cli_env`.

| `cli_env` value | Behavior |
|-----------------|----------|
| `${key}` | Inline keyring substitution |
| `@{key}` | Materialize keyring secret to `~/.ati/.creds/` (0600), env set to file path; wiped on drop in ephemeral keyring mode |
| plain string | Passthrough |

If `[[tools]]` is empty, load registers one tool named after the provider with scope `tool:{provider_name}`. Invocation passes **positional** args after the tool name to the subprocess (`cli_default_args` prepended). Proxy mode can rewrite output paths via `cli_output_args` / `cli_output_positional`, capture files, and return base64 in JSON.

Example (`manifests/google-workspace.toml`):

```toml
[provider]
name = "google_workspace"
handler = "cli"
cli_command = "gws"
cli_timeout_secs = 120
auth_type = "none"

[provider.cli_env]
GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE = "@{google_workspace_credentials}"
```

Call pattern: `ati run google_workspace -- <subcommand> <args…>`.

## `file_manager` — binary download/upload

Virtual provider registered by `register_file_manager_provider` if not fully declared by the operator. Tools are always `file_manager:download` and `file_manager:upload`.

- Operator manifest with `handler = "file_manager"` and `upload_destinations` can supply allowlisted upload sinks; built-in tools are backfilled when `[[tools]]` is empty.
- `ati run` short-circuits to `cli/file_manager` for local file I/O, then either direct `file_manager` ops (local) or proxy `POST /call` with base64 payloads (proxy).
- Proxy-only `dispatch_file_manager` in `proxy/server.rs` enforces destinations and SSRF rules from `core/file_manager`.

## `passthrough` — raw HTTP reverse proxy

Not an `ati run` handler. Enabled with `ati proxy --enable-passthrough`. `PassthroughRouter::build` compiles routes from manifests where `handler = "passthrough"`. Requires non-empty `base_url` and at least one of `host_match` or `path_prefix`.

Requests that miss named proxy routes hit `handle_passthrough`: longest-prefix match, optional `strip_prefix` / `path_replace`, `deny_paths` / `forward_authorization_paths` globs, keyring-resolved auth injected at startup, body size caps. WebSocket forwarding is rejected at load time today.

See `deploy/examples/vm/manifests/example-passthrough.toml` for a commented template.

## Authentication injection

HTTP and OpenAPI tools share `inject_auth` in `core/http.rs`. Priority: `auth_generator` (dynamic credentials from `GenContext` + `AuthCache`) over static keyring.

| `auth_type` | HTTP injection |
|-------------|----------------|
| `none` | No credential |
| `bearer` | `Authorization: Bearer` from `auth_key_name` |
| `header` | Custom header (`auth_header_name`, optional `auth_value_prefix`) |
| `query` | Query param (`auth_query_name`, default `api_key`) |
| `basic` | HTTP Basic from keyring value |
| `oauth2` | Client-credentials token (cached per provider name) |
| `url` | Key embedded in URL via `${key}` at connect time; no header/query inject |

`provider.extra_headers` are applied after auth. Agent-supplied headers matching a deny-list (including `authorization`) are rejected for classified header params.

MCP stdio: secrets via `mcp_env` `${…}`; optional `auth_generator` → `ATI_AUTH_TOKEN` + `extra_env`. CLI: same generator path into subprocess env; static secrets via `cli_env`.

Passthrough: auth frozen at router build from keyring; per-path `forward_authorization_paths` can forward sandbox `Authorization` and skip manifest inject.

<Tip>
Use `auth_generator` when credentials must be minted per call (JWT-bearing generators, short-lived tokens). Use `auth_key_name` + `ati key set` for static API keys.
</Tip>

## End-to-end call sequence

```mermaid
sequenceDiagram
  participant Agent
  participant ATI as ati run / proxy /call
  participant Reg as ManifestRegistry
  participant Exec as Handler backend

  Agent->>ATI: tool_name + args
  ATI->>Reg: get_tool(name)
  alt MCP prefix, not indexed
    ATI->>Exec: discover + register_mcp_tools
  end
  Reg-->>ATI: Provider + Tool
  ATI->>ATI: normalize_arg_keys, scope check
  alt handler mcp
    ATI->>Exec: strip prefix, McpClient.call_tool
  else handler cli
    ATI->>Exec: subprocess + cli_env
  else handler file_manager
    ATI->>Exec: fetch/upload bytes
  else http or openapi
    ATI->>Exec: classify_params, inject_auth, reqwest
  end
  Exec-->>Agent: JSON / formatted text
```

## Operator checklist

<Steps>
<Step title="Pick a handler">
Use `http` for a few fixed endpoints, `openapi` for large REST APIs, `mcp` for MCP servers, `cli` for existing CLIs, `file_manager` for agent binary I/O, `passthrough` only on the proxy for transparent HTTP proxying.
</Step>
<Step title="Author the manifest">
Place `~/.ati/manifests/<name>.toml`. For OpenAPI, run `ati provider import-openapi` so the spec lives under `~/.ati/specs/`.
</Step>
<Step title="Store credentials">
`ati key set <auth_key_name> <secret>` for `${…}` / `@{…}` references and HTTP auth fields.
</Step>
<Step title="Verify discovery">
`ati tool list` (triggers MCP discovery) and `ati tool info <exact_tool_name>` for schema and scope.
</Step>
<Step title="Run scoped">
`ati run <tool_name> --arg value` with a JWT or local scope env that includes `tool:<name>` or a matching wildcard.
</Step>
</Steps>

## Troubleshooting

| Symptom | Likely cause |
|---------|----------------|
| Unknown tool after MCP add | Discovery not run; prefix wrong; typo in `provider:tool` segment |
| `Unknown tool: 'finnhub:foo'` with suggestions | OpenAPI cap/filter excluded operation; spec not loaded (check proxy logs for warn) |
| `hackernews:top_stories` not found | Hand-written tool uses flat name `hackernews_top_stories` |
| MCP auth failures in proxy mode | Keyring empty on proxy host; `mcp_env` key missing |
| CLI credential errors | `@{key}` not in keyring; file permissions under `~/.ati/.creds` |
| Passthrough 404 | `--enable-passthrough` off; no matching `host_match`/`path_prefix` |
| Scope denied | Auto scope `tool:{name}` not in JWT; use wildcard `tool:provider:*` for MCP families |

## Related pages

<CardGroup>
<Card title="Manifest reference" href="/manifest-reference">
Full TOML schema for `[provider]`, `[[tools]]`, handler-specific fields, and load-time validation errors.
</Card>
<Card title="Import OpenAPI" href="/import-openapi">
Download specs, filters, `x-ati-param-location`, and operation caps.
</Card>
<Card title="Add MCP providers" href="/add-mcp-providers">
`ati provider add-mcp`, stdio vs HTTP, env injection, namespaced calls.
</Card>
<Card title="CLI and HTTP manifests" href="/cli-and-http-manifests">
`add-cli`, hand-written HTTP, `${key}` vs `@{key}`, output capture.
</Card>
<Card title="File manager operations" href="/file-manager-operations">
Download/upload tools, SSRF, upload destinations, proxy base64 transfer.
</Card>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
Passthrough flag, sig-verify, route compilation, health checks.
</Card>
</CardGroup>

---

## 06. Scopes and tool discovery

> JWT `scope` claims, wildcard patterns (`tool:github:*`), scope-filtered listing, and the three discovery tiers: `ati tool search`, `ati tool info`, and `ati assist`.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/06-scopes-and-tool-discovery.md
- Generated: 2026-06-02T03:15:14.489Z

### Source Files

- `src/core/scope.rs`
- `src/core/jwt.rs`
- `src/cli/tools.rs`
- `src/cli/help.rs`
- `src/proxy/server.rs`
- `docs/assist-examples.md`

---
title: "Scopes and tool discovery"
description: "JWT `scope` claims, wildcard patterns (`tool:github:*`), scope-filtered listing, and the three discovery tiers: `ati tool search`, `ati tool info`, and `ati assist`."
---

ATI binds every public tool to a canonical scope string (typically `tool:<tool_name>`) and enforces access through the JWT `scope` claim—a space-delimited list parsed into `ScopeConfig` in `src/core/scope.rs`. The same filter drives `ati tool list|search|info`, proxy `GET /tools`, and the tool catalog fed to `ati assist`; `ati run` and proxy `POST /call` call `check_access` before dispatch. Without JWT keys configured locally, discovery runs unrestricted (`ScopeConfig::unrestricted()` with `*`).

## Scope model

### JWT claim → `ScopeConfig`

`TokenClaims.scope` (RFC 9068 §2.2.3) is split on whitespace into individual scope tokens. `ScopeConfig::from_jwt` copies those tokens plus `sub`, `exp`, and optional per-pattern rate limits from the `ati` namespace (`ati.rate` map).

| Field | Source | Role |
|-------|--------|------|
| `scopes` | JWT `scope` | Allowlist patterns for tools, skills, and special tokens |
| `sub` | JWT `sub` | Agent identity |
| `expires_at` | JWT `exp` | `0` = no expiry; expired scopes deny all `is_allowed` checks |
| `rate_config` | `ati.rate` | Optional per-tool-pattern limits at call time |

Load scopes in local mode via `common::load_local_scopes_from_env()`:

- If `ATI_JWT_PUBLIC_KEY` or `ATI_JWT_SECRET` is set, `ATI_SESSION_TOKEN` (or `ATI_SESSION_TOKEN_FILE`, or `/run/ati/session_token`) must validate.
- If JWT validation is not configured, commands use unrestricted dev scopes.

Proxy mode (`ATI_PROXY_URL` set) forwards `ati tool` to `GET /tools` and `GET /tools/:name`; the proxy builds `ScopeConfig` from the Bearer JWT, or unrestricted when `jwt_config` is unset, or empty scopes when JWT is required but missing.

### Tool scope strings on manifests

At manifest load, any public tool without an explicit `scope` field receives `tool:{tool.name}` automatically (`ManifestRegistry` in `src/core/manifest.rs`). OpenAPI-imported tools default to `tool:{prefixed_name}` unless overridden. Internal providers (`internal = true`, e.g. `_llm` for assist) are excluded from `list_public_tools()` and never appear in discovery output.

Hand-written manifests can set `scope` per `[[tools]]` entry—for example, grouping Hacker News endpoints under one scope:

```toml
[[tools]]
name = "hackernews_top"
scope = "tool:hackernews_stories"
```

MCP-discovered tools use the namespaced tool name (`github:search_repositories`) and scope `tool:github:search_repositories`.

### Matching rules

`ScopeConfig::is_allowed(tool_scope)` applies these rules in order:

1. **Expired token** — all denied.
2. **Empty tool scope** — always allowed (tool has no scope requirement).
3. **Pattern match** — each JWT scope token is checked with `matches_wildcard`:
   - Exact: `tool:web_search` matches `tool:web_search`
   - Suffix wildcard: `tool:github:*` matches any scope starting with `tool:github:`
   - Global: `*` matches everything
4. **Legacy underscore alias** — colon-namespaced tool scopes also match underscore forms from older tokens, e.g. JWT `tool:github_*` matches tool scope `tool:github:search_repos`, and JWT `tool:test_api_get_data` matches `tool:test_api:get_data`.

`check_access(tool_name, tool_scope)` returns `ScopeError::AccessDenied` or `ScopeError::Expired` for `ati run` and proxy `/call`.

```mermaid
flowchart LR
  subgraph token [JWT]
    scopeClaim["scope claim"]
  end
  subgraph runtime [ATI runtime]
    SC["ScopeConfig"]
    FT["filter_tools_by_scope"]
    CA["check_access"]
  end
  subgraph surfaces [Surfaces]
    LIST["ati tool list/search"]
    INFO["ati tool info"]
    RUN["ati run / POST /call"]
    ASSIST["ati assist / POST /help"]
  end
  scopeClaim --> SC
  SC --> FT
  FT --> LIST
  FT --> INFO
  FT --> ASSIST
  SC --> CA
  CA --> RUN
```

## Scope grammar

| Token pattern | Example | Effect |
|---------------|---------|--------|
| `tool:<name>` | `tool:web_search` | Allows one tool whose manifest scope equals the token |
| `tool:<provider>:*` | `tool:github:*` | Allows all tools under that provider prefix |
| `tool:<provider>_<tool>` | `tool:github_search_repositories` | Legacy alias for `tool:github:search_repositories` |
| `skill:<name>` | `skill:research-general` | Skill visibility and resolution (not tool listing) |
| `help` | `help` | Sets `help_enabled` on `ScopeConfig` (reported by `ati auth status`) |
| `*` | `*` | Unrestricted tools; `help_enabled` true |

<Note>
`ati auth status` reports `help_enabled` when `help` or `*` is present. Include `help` in issued tokens when agents should use assist-heavy workflows (`ati init --proxy` examples use `"tool:* help"`). Tool listing and assist context are always filtered by **tool** scopes regardless of the `help` flag.
</Note>

### Issue a scoped token

```bash
ati token issue \
  --sub agent-sandbox-1 \
  --scope "tool:web_search tool:github:* help" \
  --ttl 3600 \
  --secret "$ATI_JWT_SECRET"

export ATI_SESSION_TOKEN="<printed token>"
ati auth status
```

Inspect without verification: `ati token inspect <token>`. Validate signature: `ati token validate <token>`.

## Scope-filtered listing

`filter_tools_by_scope` keeps tools where `tool.scope` is absent or `scopes.is_allowed(scope)`; wildcard `*` skips filtering. Both CLI and proxy discovery call this on `registry.list_public_tools()` (non-internal providers only).

**Proxy without JWT:** if `jwt_config` is configured and no Bearer token is sent, `scopes_for_request` returns empty scopes—list/info return no tools and `/call` returns 403. With no `jwt_config`, proxy runs unrestricted.

**Local without JWT keys:** unrestricted dev mode—all public tools visible.

Optional provider filter on list: `ati tool list --provider finnhub`.

## Three discovery tiers

Discovery is deliberately tiered: cheap deterministic search first, schema detail second, LLM synthesis last.

```text
  Agent question
       │
       ├─► Tier 1: ati tool search "<terms>"     fuzzy rank, top 20, scope-filtered
       │
       ├─► Tier 2: ati tool info <name>          schema, tags, skills, usage line
       │
       └─► Tier 3: ati assist "<question>"       LLM + scoped tool catalog (+ skills)
```

### Tier 1 — `ati tool search`

```bash
ati tool search "stock price quote"
ati tool search "github pull request" --output json
```

**Behavior (local):**

1. Load manifests, discover MCP tools (`discover_mcp_tools`).
2. `filter_tools_by_scope`.
3. Score each tool with `score_tool_match` over name (weight 10/5), provider (3), category (3), tags (4), description (2), hint (1.5).
4. Fuzzy match uses Jaro–Winkler ≥ 0.85 for terms ≥ 4 characters; stop words stripped.
5. Require at least half of content terms to match; sort by score; **truncate to 20** results.

**Proxy mode:** `GET /tools?search=<query>` applies scope filtering then **substring** match on name, description, and tags (no fuzzy rank). Prefer local search when you need ranked fuzzy results behind a proxy.

### Tier 2 — `ati tool info`

```bash
ati tool info finnhub__quote
ati tool info gh --output json
```

Resolves the tool only if in scope; otherwise: `Unknown tool: '<name>'. Run 'ati tool list' to see available tools.`

**Output includes:** provider, handler, endpoint or MCP/CLI details, `scope`, tags, hint, bound skills (manifest + `SkillRegistry`), `input_schema`, examples, and a generated `ati run` usage line. CLI tools without `input_schema` capture live `--help` (≤ 3000 chars) via `capture_cli_help`.

Proxy equivalent: `GET /tools/:name` with the same scope gate (404 if not allowed).

### Tier 3 — `ati assist`

```bash
# Unscoped — natural-language discovery across visible tools
ati assist "do we have a tool for stock prices?"

# Scoped to one tool or whole provider (first arg must match registry)
ati assist finnhub "which endpoint gives real-time quotes?"
ati assist gh "how do I create a pull request?"
```

**Routing:**

| Environment | Path |
|-------------|------|
| `ATI_PROXY_URL` set | `POST /help` with `{ query, tool? }` |
| Local | Internal `_llm` provider + keyring API key (or `--local` for Ollama) |

**Scope handling:**

1. `filter_tools_by_scope` on all public tools.
2. Unscoped: `prefilter_tools_by_query` keeps up to **50** tools using the same scorer as search; builds LLM system prompt from that subset plus resolved skills.
3. Scoped: first arg matching a tool name or provider name narrows context; if not visible, error (`'X' is not visible in your current scopes` locally; HTTP 403 on proxy).

Proxy `/help` also resolves skills via `skill::resolve_skills` and optional remote SkillATI sections. Responses are concise prose with embedded `ati run` examples; JSON output adds `tools_referenced`. See `docs/assist-examples.md` for sample shapes.

<Warning>
Assist uses the internal `_llm` manifest. It does not appear in `ati tool list`. Provision an LLM API key in the keyring for local assist; the proxy requires `_llm.toml` and a keyring entry for `/help`.
</Warning>

### Tier comparison

| Tier | Command | Cost | Best for |
|------|---------|------|----------|
| 1 | `ati tool search` | No LLM | Keyword discovery, scanning candidates |
| 2 | `ati tool info` | No LLM | Parameters, auth shape, exact `ati run` syntax |
| 3 | `ati assist` | LLM call | Multi-step workflows, “which tool should I use?” |

## Execution vs discovery

Discovery filters are **read-only**. Calling a tool still requires scope at execution:

```bash
# Discovery sees only scoped tools
ati tool list

# Execution enforces the same scope
ati run finnhub__quote --symbol AAPL
```

`ati run` loads scopes and calls `scopes.check_access(tool_name, tool.scope)` before dispatch. Proxy `/call` returns 403 with `Access denied: '<tool>' is not in your scopes` when the JWT lacks the tool’s scope pattern.

Rate limits from `ati.rate` in the JWT apply at call time (`core/rate.rs`), not during listing.

## Verify scopes and discovery

<Steps>
<Step title="Check session scopes">

```bash
ati auth status
```

Confirm tool count, raw `scope` string, expiry, and `help_enabled`.

</Step>
<Step title="List and search within scope">

```bash
ati tool list
ati tool search "quote"
```

Empty output usually means over-narrow scopes or missing manifests/MCP discovery.

</Step>
<Step title="Inspect one tool">

```bash
ati tool info <tool_name>
```

Confirm `scope` field matches a token you can issue (e.g. `tool:github:*` for `tool:github:search_repositories`).

</Step>
<Step title="Optional assist smoke test">

```bash
ati assist "what tools can fetch stock quotes?"
```

Requires LLM key (local) or proxy `/help` setup.

</Step>
</Steps>

## Common failure modes

| Symptom | Likely cause |
|---------|----------------|
| `ati tool list` empty | Token scopes omit needed `tool:` patterns; proxy JWT required but missing; MCP discovery failed |
| `Unknown tool` on `ati tool info` | Tool exists but scope not allowed; typo in namespaced MCP name |
| `Access denied` on `ati run` | Token valid for discovery mismatch—check exact `tool.scope` on manifest |
| `ATI_SESSION_TOKEN is required` locally | JWT validation configured without token |
| Assist 503 / no LLM | Missing `_llm` manifest or API key in keyring |
| Scoped assist forbidden | `ati assist <scope>` where scope not in filtered visible set |

<Tip>
For provider-wide sandboxes, prefer `tool:<provider>:*` over enumerating each MCP tool. For single-tool agents, issue `tool:<exact_tool_name>` matching the manifest `scope` field from `ati tool info`.
</Tip>

## Related pages

<CardGroup>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
Issue and validate session tokens, keygen, and orchestrator patterns.
</Card>
<Card title="JWT and scopes reference" href="/jwt-scopes-reference">
Full claims grammar, validation errors, and scope token reference.
</Card>
<Card title="Assist and plan reference" href="/assist-and-plan-reference">
`--plan`, `--save`, `--local`, and saved plan execution.
</Card>
<Card title="Proxy API reference" href="/proxy-api-reference">
`GET /tools`, `POST /help`, and `/call` request shapes.
</Card>
<Card title="Agent harness sandbox" href="/agent-harness-sandbox">
Provision sandboxes with scoped `ATI_SESSION_TOKEN` injection.
</Card>
</CardGroup>

---

## 07. Skills and SkillATI

> Skills as methodology docs vs tools as data access, progressive disclosure (metadata → SKILL.md → resources), scope-driven resolution cascade, and remote GCS registry via `/skillati/*`.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/07-skills-and-skillati.md
- Generated: 2026-06-02T03:15:09.299Z

### Source Files

- `src/core/skill.rs`
- `src/core/skillati.rs`
- `src/cli/skills.rs`
- `src/cli/skillati.rs`
- `skills/google-workspace/SKILL.md`
- `skills/google-workspace/skill.toml`

---
title: "Skills and SkillATI"
description: "Skills as methodology docs vs tools as data access, progressive disclosure (metadata → SKILL.md → resources), scope-driven resolution cascade, and remote GCS registry via `/skillati/*`."
---

ATI separates **tools** (manifest-backed API/MCP/CLI calls that return data) from **skills** (methodology documents under `~/.ati/skills/` or a remote SkillATI bucket). Skills teach agents *how* to use tools; they never replace manifests. Local skills load from `SkillRegistry` in `src/core/skill.rs`; remote skills flow through `SkillAtiClient` in `src/core/skillati.rs` and proxy routes under `/skillati/*`. Progressive disclosure keeps token use bounded: catalog metadata first, `SKILL.md` body on activation, `scripts/` / `references/` / `assets/` only when the body or agent requests them.

## Skills vs tools

| Surface | Role | Defined in | Agent access |
|---------|------|------------|--------------|
| Tool | Data/API call with schema | `manifests/*.toml` (`[[tools]]` or OpenAPI/MCP discovery) | `ati run <tool> --arg value` |
| Skill | Workflow, examples, guardrails | `SKILL.md` + optional `skill.toml` per skill directory | `ati skill read`, `ati skill fetch read`, or proxy `/skillati/:name` |

Skills reference tools, providers, and categories through bindings in metadata — not the reverse. Installing a skill does not edit manifests.

<Note>
The bundled `google-workspace` skill binds `providers = ["google_workspace"]` in `skill.toml` and documents `ati run google_workspace -- …` patterns in `SKILL.md`. A JWT with `tool:google_workspace:*` or `skill:google-workspace` can surface that skill without changing the provider manifest.
</Note>

## Skill directory layout

Each skill is one subdirectory under `~/.ati/skills/<name>/` (or a GCS prefix `<skill-name>/` in a bucket):

:::files
~/.ati/skills/<name>/
├── SKILL.md          # Methodology body; optional YAML frontmatter (Anthropic spec)
├── skill.toml        # Optional ATI bindings ([skill] table)
├── references/       # Level-3 docs (on demand)
├── scripts/          # Level-3 helpers (on demand)
└── assets/           # Level-3 templates/data (on demand)
:::

Metadata priority when loading locally: YAML frontmatter in `SKILL.md` → `skill.toml` → inferred from `SKILL.md` body. Skill names must satisfy Anthropic rules (1–64 chars, lowercase letters, digits, hyphens; no consecutive hyphens).

## Progressive disclosure

ATI mirrors the Agent Skills three-level model used by Claude Code:

| Level | Payload | When loaded | ATI surface |
|-------|---------|-------------|-------------|
| 1 — Metadata | `name`, `description`, optional `when_to_use` | Agent provisioning / catalog listing | `GET /skillati/catalog`, `ati skill fetch catalog`, orchestrator `build_skill_listing` → `<system-reminder>` block (250 chars/entry, 8000 chars/body budget on the Python client) |
| 2 — Instructions | `SKILL.md` body (frontmatter stripped), `skill_directory`, `description` | Agent activates a skill | `GET /skillati/:name`, `ati skill fetch read <name>` → `SkillAtiActivation` |
| 3 — Resources | Files under `references/`, `scripts/`, `assets/` | On demand per SKILL.md or explicit fetch | `ati skill fetch resources\|cat\|refs\|ref`, `/skillati/:name/resources`, `/file`, `/refs`, `/ref/:reference` |

`SkillAtiActivation` intentionally omits a resource manifest at Level 2. Agents list or read Level 3 explicitly.

```mermaid
sequenceDiagram
    participant Agent
    participant Proxy as ati proxy
    participant SkillATI as SkillAtiClient
    participant GCS as GCS bucket

    Agent->>Proxy: GET /skillati/catalog (JWT)
    Proxy->>SkillATI: catalog()
    SkillATI->>GCS: _skillati/catalog.v1.json or list prefixes
    GCS-->>SkillATI: RemoteSkillMeta[]
    Proxy-->>Agent: skills[] (scope-filtered)

    Agent->>Proxy: GET /skillati/:name
    Proxy->>SkillATI: read_skill(name)
    SkillATI->>GCS: GET :name/SKILL.md
    SkillATI-->>Proxy: SkillAtiActivation (substituted body)
    Proxy-->>Agent: name, description, skill_directory, content

    Agent->>Proxy: GET /skillati/:name/file?path=scripts/foo.sh
    Proxy->>SkillATI: read_path(name, path)
    SkillATI->>GCS: GET object
    Proxy-->>Agent: SkillAtiFile (text or base64)
```

## Local skill registry

`SkillRegistry::load` scans `~/.ati/skills/*/`, builds indexes by name, tool, provider, and category, and backs CLI commands in `src/cli/skills.rs`.

| Command | Purpose |
|---------|---------|
| `ati skill list` | List installed skills (filters: `--category`, `--provider`, `--tool`) |
| `ati skill show <name>` | Show `SKILL.md` (`--meta`, `--refs`) |
| `ati skill info <name>` | Structured metadata |
| `ati skill read <name>` | Raw body for agents (`--tool`, `--with-refs`) |
| `ati skill resolve` | Skills auto-loaded for scopes (`--scopes` path) |
| `ati skill install\|remove\|init\|validate\|verify\|diff\|update` | Lifecycle (always local filesystem) |

When `ATI_PROXY_URL` is set, read-only skill commands (`list`, `show`, `search`, `info`, `read`, `resolve`) forward to proxy `/skills` endpoints; install/remove/init/validate stay local.

`build_skill_context` caps injected skill metadata at 32 KiB for `ati assist` system prompts.

## Scope-driven resolution cascade

`skill::resolve_skills` and `skill::visible_skills` determine which local skills appear for a JWT `scope` claim:

1. **Explicit** — `skill:X` loads skill `X` directly.
2. **Tool binding** — `tool:Y` loads skills whose `tools[]` includes `Y`.
3. **Provider binding** — tool `Y`’s provider `P` loads skills with `providers[]` containing `P`.
4. **Category binding** — provider category `C` loads skills with `categories[]` containing `C`.
5. **Dependencies** — each loaded skill’s `depends_on[]` is transitively loaded.

Wildcard scope `*` sees the full local registry but does not auto-load every skill into assist context. A second pass via `filter_tools_by_scope` maps legacy underscore tool scopes to colon-namespaced manifest tools.

Remote catalog visibility uses the same cascade in `visible_remote_skill_names` (`src/proxy/server.rs`): explicit `skill:X`, then tool/provider/category bindings against `RemoteSkillMeta` entries. `visible_skill_names_with_remote` unions local and remote names before `/skillati/*` handlers run.

<Warning>
A token with only `skill:foo` can list and read remote `foo` but cannot read remote `bar`, even if `bar` appears in a cross-skill URI inside `foo`’s body — each `fetch read` / `/skillati/:name` call re-checks visibility.
</Warning>

Proxy `POST /skills/resolve` accepts `{ "scopes": [...], "include_content": bool }`, resolves via `resolve_skills`, intersects with `visible_skill_names` (local only on that route), and optionally embeds full `SKILL.md` content.

## SkillATI remote registry

SkillATI is the lazy GCS (or proxy-mediated) registry for skills not installed under `~/.ati/skills/`.

### Configuration

| Variable | Values | Behavior |
|----------|--------|----------|
| `ATI_SKILL_REGISTRY` | unset | SkillATI disabled; `/skillati/*` returns 503 on proxy |
| | `gcs://<bucket>` | Direct GCS via `gcp_credentials` keyring key |
| | `proxy` | Client calls proxy `/skillati/*`; requires `ATI_PROXY_URL` |
| `ATI_SKILL_REGISTRY_INDEX_OBJECT` | object path | Override catalog index location |
| `ATI_SKILL_FETCH_DISABLED` | `1` / `true` | Blocks `ati skill fetch` (not `build-index`); for sandboxes with pre-installed skills |
| `ATI_PROXY_URL` | URL | Routes `ati skill fetch` and read-only `ati skill` to proxy |

GCS layout: one prefix per skill (`<skill-name>/SKILL.md`, …). Catalog index candidates (first hit wins): `_skillati/catalog.v1.json`, `_skillati/catalog.json`, `skillati-catalog.json`. Without an index, ATI lists bucket prefixes and parses each `SKILL.md` (up to 24 concurrent).

Publish a catalog with:

```bash
ati skill fetch build-index --source-dir ./skills --output-file catalog.v1.json
# Upload to GCS as _skillati/catalog.v1.json (recommended)
```

### Level-2 activation shape

`ati skill fetch read <name>` (alias: `ati skillati read`) returns JSON:

<ResponseField name="name" type="string">
Skill identifier.
</ResponseField>

<ResponseField name="description" type="string">
One-line description from catalog entry when available.
</ResponseField>

<ResponseField name="skill_directory" type="string">
Virtual base URI, always `skillati://<name>`.
</ResponseField>

<ResponseField name="content" type="string">
`SKILL.md` body after frontmatter strip and `substitute_skill_refs`.
</ResponseField>

Text output uses the Claude Code preamble:

```text
Base directory for this skill: skillati://<name>

<description>

<SKILL.md body>
```

### Reference substitution

`substitute_skill_refs` rewrites paths so Claude-authored skills work in sandboxes without local `.claude/skills/`:

1. `${ATI_SKILL_DIR}` and `${CLAUDE_SKILL_DIR}` → `skillati://<current-skill>/`
2. `.claude/skills/<other>/…` (valid skill name) → `skillati://<other>/…`
3. Bare `<catalog-skill>/(references|scripts|assets)/…` → `skillati://<catalog-skill>/…` when the first segment is in the remote catalog

Prose mentions like "the `.claude/skills/` directory" are left unchanged (`is_anthropic_valid_name` guard).

### CLI: `ati skill fetch`

Subcommands (also under `ati skillati` if exposed):

| Subcommand | Remote action |
|------------|---------------|
| `catalog [--search Q]` | List/filter `RemoteSkillMeta` |
| `read <name>` | Level-2 activation |
| `resources <name> [--prefix P]` | List paths without content |
| `cat <name> <path>` | Read arbitrary skill-relative file |
| `refs <name>` | List `references/*` basenames |
| `ref <name> <reference>` | Read `references/<reference>` |
| `build-index` | Offline manifest generation (local only) |

With `ATI_PROXY_URL`, fetch uses the same paths on the proxy with `ATI_SESSION_TOKEN` Bearer auth.

## Proxy HTTP routes

### Local skills (`~/.ati/skills/`)

| Method | Path | Description |
|--------|------|-------------|
| GET | `/skills` | List skills (`?category`, `?provider`, `?tool`, `?search`) |
| GET | `/skills/:name` | Detail (`?meta`, `?refs`) |
| GET | `/skills/:name/bundle` | All files in skill directory |
| POST | `/skills/bundle` | Multi-skill bundle |
| POST | `/skills/resolve` | Scope → resolved skill metadata (+ optional content) |

### SkillATI (remote)

| Method | Path | Description |
|--------|------|-------------|
| GET | `/skillati/catalog` | Scope-filtered catalog (`?search`) |
| GET | `/skillati/:name` | Level-2 `SkillAtiActivation` |
| GET | `/skillati/:name/resources` | Resource paths (`?prefix`) |
| GET | `/skillati/:name/file` | Single file (`?path=`) → `SkillAtiFile` |
| GET | `/skillati/:name/refs` | Reference basenames |
| GET | `/skillati/:name/ref/:reference` | Reference file content |

All SkillATI routes require `ATI_SKILL_REGISTRY` on the proxy and JWT scope gating. Unconfigured registry → 503; out-of-scope skill → 404 (`SkillNotFound`).

:::endpoint GET /skillati/catalog
Scope-filtered remote skill catalog. Response: `{ "skills": [ RemoteSkillMeta, ... ] }`. Optional `search` query applies fuzzy filter (limit 25).
:::

:::endpoint GET /skillati/{name}
Level-2 activation for one remote skill. Response fields: `name`, `description`, `skill_directory`, `content`. No `resources` array.
:::

## Orchestrator integration (Level 1)

`AtiOrchestrator.build_skill_listing` (Python `ati-client`) calls `GET {proxy}/skillati/catalog` with the sandbox JWT and formats entries into a `<system-reminder>` block — matching Claude Code’s skill listing shape for agent provisioning without loading full `SKILL.md` bodies.

## Security notes

- Remote skill bodies can reach LLM context via assist or explicit read; audit registry content for prompt injection.
- GCS mode: `gcp_credentials` in the keyring; proxy mode keeps GCP keys off sandboxes.
- `read_skill` content is bounded by object size; local `build_skill_context` enforces 32 KiB for metadata injection.

<Info>
Provider-neutral: skills are plain directories or bucket objects. Any agent runtime that can call ATI CLI or proxy HTTP can use the same SkillATI URIs and progressive-disclosure contract without binding to a single model vendor.
</Info>

## Example binding

`skills/google-workspace/skill.toml`:

```toml
[skill]
name = "google-workspace"
providers = ["google_workspace"]
categories = ["productivity"]
```

`SKILL.md` documents `ati run google_workspace -- drive files list` and related flows. With `tool:google_workspace:*` in the JWT, resolution step 3 loads this skill; with `skill:google-workspace`, step 1 loads it directly.

## Troubleshooting

| Symptom | Likely cause |
|---------|----------------|
| `/skillati/*` 503 | `ATI_SKILL_REGISTRY` unset on proxy |
| Remote skill 404 with valid scope | Empty local `skills/` and missing remote binding; check `skill:X` or tool/provider/category cascade |
| `ati skill fetch` exits 1 | `ATI_SKILL_FETCH_DISABLED` set in sandbox |
| Empty catalog | Wrong bucket, missing index, or invalid `gcp_credentials` |
| Substitution not applied | Skill name not in catalog (rule 3); invalid `.claude/skills/<name>` segment |

## Related pages

<CardGroup>
<Card title="Scopes and tool discovery" href="/scopes-and-tool-discovery">
JWT scopes, wildcards, and how tool scopes drive skill visibility.
</Card>
<Card title="Skills registry and fetch" href="/skills-registry-and-fetch">
Install, resolve, GCS publishing, and fetch command details.
</Card>
<Card title="Proxy API reference" href="/proxy-api-reference">
Full `/skills` and `/skillati/*` request shapes and auth.
</Card>
<Card title="JWT and scopes reference" href="/jwt-scopes-reference">
`skill:` and `tool:` scope grammar.
</Card>
<Card title="Environment variables" href="/environment-variables">
`ATI_SKILL_REGISTRY`, `ATI_SKILL_FETCH_DISABLED`, and related env contract.
</Card>
</CardGroup>

---

## 08. Import OpenAPI providers

> `ati provider import-openapi` and `inspect-openapi`: spec download, operation caps, tag filters, `x-ati-param-location` routing, and keyring key hints.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/08-import-openapi-providers.md
- Generated: 2026-06-02T03:15:08.963Z

### Source Files

- `src/cli/provider.rs`
- `src/core/openapi.rs`
- `manifests/finnhub.toml`
- `specs/finnhub.json`
- `specs/crossref.json`
- `src/core/http.rs`

---
title: "Import OpenAPI providers"
description: "`ati provider import-openapi` and `inspect-openapi`: spec download, operation caps, tag filters, `x-ati-param-location` routing, and keyring key hints."
---

`ati provider import-openapi` downloads an OpenAPI 3.x spec (JSON or YAML) into `~/.ati/specs/`, writes an `handler = "openapi"` manifest under `~/.ati/manifests/`, and relies on `ManifestRegistry` to auto-register tools at load time. `ati provider inspect-openapi` previews the same spec without writing files. At runtime, `core/openapi.rs` injects `x-ati-param-location` into each tool’s input schema and `core/http.rs` routes arguments to path, query, header, or body.

## Commands

| Command | Purpose |
|---------|---------|
| `ati provider inspect-openapi <spec>` | Print title, base URL, detected auth, and operations grouped by tag |
| `ati provider import-openapi <spec>` | Download spec, generate manifest, save both under `~/.ati/` |

<ParamField body="spec" type="string" required>
Path or URL to an OpenAPI 3.x document (`.json`, `.yaml`, or `.yml`).
</ParamField>

<ParamField body="--name" type="string">
Provider name. When omitted, derived from the URL host or file stem (for example `https://api.finnhub.io/...` → `finnhub`, `finnhub.json` → `finnhub`).
</ParamField>

<ParamField body="--auth-key" type="string">
Keyring entry name for API credentials. Default: `{name}_api_key` (for example `finnhub_api_key`).
</ParamField>

<ParamField body="--include-tags" type="string[]">
Repeatable. Restrict which operations are counted during import and which tag filter is written into the manifest. During inspect, filters the printed operation list only.
</ParamField>

<ParamField body="--dry-run" type="boolean">
Print the generated manifest and paths that would be written; do not create files.
</ParamField>

<Note>
`--auth-key` is only available on `import-openapi`, not `inspect-openapi`.
</Note>

### Spec input: URL vs local file

`read_spec_content` in `cli/provider.rs` handles both:

- **URL**: SSRF validation via `validate_url_not_private`, 30s timeout, redirects disabled (a 3xx returns an error naming the `Location` target). HTTP URLs log an insecure-transport warning.
- **Local path**: read directly from disk.

At **registry load** time, `openapi::load_spec` does not fetch URLs. The spec must already live under `~/.ati/specs/` (typically from `import-openapi`). A manifest pointing at a bare URL without a local copy fails with guidance to import first.

## Workflow

<Steps>
<Step title="Inspect the spec">
Preview operations and auth before committing files.

```bash
ati provider inspect-openapi https://petstore3.swagger.io/api/v3/openapi.json
ati provider inspect-openapi ./specs/crossref.json --include-tags Members
```

Output includes API title/version, first `servers[0].url`, detected `auth_type` plus `auth_header_name` or `auth_query_name` when applicable, and operations grouped by OpenAPI tag.
</Step>

<Step title="Import (or dry-run)">
Generate the manifest and copy the normalized spec.

```bash
ati provider import-openapi https://finnhub.io/api/v1/openapi.json --name finnhub
ati provider import-openapi https://api.crossref.org/swagger.json --dry-run
```

Writes:

- `~/.ati/specs/{name}.json` — pretty-printed parsed spec
- `~/.ati/manifests/{name}.toml` — provider block only (no hand-written `[[tools]]`)
</Step>

<Step title="Store credentials">
When `auth_type` is not `none`, import logs a hint:

```bash
ati key set finnhub_api_key <your-token>
```

The manifest sets `auth_key_name` to your `--auth-key` value or `{name}_api_key` by default.
</Step>

<Step title="Verify tools">
```bash
ati provider list
ati tool list | grep finnhub
ati tool info finnhub:quote
```

OpenAPI tools are named `{provider}:{operationId}` (colon separator). If `operationId` is missing, ATI generates one from method and path (for example `get_pet_petId`).
</Step>
</Steps>

### Ephemeral alternative: `ati provider load`

`ati provider load <spec> --name <name>` caches the provider under `~/.ati/cache/providers/` for a TTL without writing permanent manifests. Use `--save` to delegate to `import-openapi`. See [Execution modes](/execution-modes) for when cached vs permanent providers apply.

## Generated manifest

`import-openapi` builds a minimal `[provider]` block:

| Field | Source |
|-------|--------|
| `name` | `--name` or derived |
| `description` | `info.title` from the spec |
| `handler` | always `openapi` |
| `base_url` | first entry in `servers` |
| `openapi_spec` | `{name}.json` (filename under `~/.ati/specs/`) |
| `auth_type` | first `components.securitySchemes` entry |
| `auth_key_name` | set when auth ≠ `none` |
| `auth_header_name` / `auth_query_name` | from API key scheme location |
| `openapi_include_tags` | only if `--include-tags` was passed |

<Warning>
Import does **not** set `openapi_max_operations`, `openapi_exclude_tags`, `openapi_include_operations`, or `openapi_exclude_operations`. Add those manually after import when you need tighter control over large specs.
</Warning>

### Example: Finnhub (auth + cap)

Bundled `manifests/finnhub.toml` shows post-import tuning:

```toml
[provider]
name = "finnhub"
handler = "openapi"
base_url = "https://finnhub.io/api/v1"
openapi_spec = "finnhub.json"
auth_type = "query"
auth_query_name = "token"
auth_key_name = "finnhub_api_key"
openapi_max_operations = 50
```

### Example: Crossref (no auth)

`manifests/crossref.toml` is a no-credential OpenAPI provider:

```toml
[provider]
name = "crossref"
handler = "openapi"
base_url = "https://api.crossref.org"
openapi_spec = "crossref.json"
auth_type = "none"
```

## Auth detection

`openapi::detect_auth` reads `components.securitySchemes` and maps the **first** scheme to ATI auth:

| OpenAPI scheme | `auth_type` | Extra manifest fields |
|----------------|-------------|------------------------|
| HTTP `bearer` | `bearer` | — |
| HTTP `basic` | `basic` | — |
| API key in header | `header` | `auth_header_name` |
| API key in query | `query` | `auth_query_name` |
| OAuth2 client credentials | `oauth2` | `oauth2_token_url` |
| API key in cookie | `none` | — |
| OpenID Connect | `none` | manual configuration required |

Cookie and OpenID schemes fall through to `none`; adjust the manifest by hand when the spec’s security does not match a supported mapping.

## Operation filters and caps

Filters apply in `openapi::extract_tools` when `ManifestRegistry` loads the provider (and when import counts operations for dry-run output). Order of evaluation:

1. `openapi_include_operations` — if non-empty, only listed `operationId`s pass
2. `openapi_exclude_operations`
3. `openapi_include_tags` — if non-empty, operation must have at least one matching tag
4. `openapi_exclude_tags` — drop if any tag matches
5. Skip `multipart/form-data` request bodies
6. `openapi_max_operations` — truncate to the first N tools after the above filters

<Info>
`openapi_max_operations` keeps the first N operations in spec iteration order. Because paths are stored in a hash map, that order can vary between runs—prefer tag or operation ID filters when you need deterministic subsets.
</Info>

`--include-tags` on import only persists `openapi_include_tags` in the manifest; exclude lists and caps are manifest-only fields documented in [Manifest reference](/manifest-reference).

### HTTP method mapping

OpenAPI `patch` maps to ATI `PUT` because `HttpMethod` has no PATCH variant. Unresolved `$ref` path items are skipped.

## Tool registration at load time

```mermaid
flowchart LR
  subgraph ati_home ["~/.ati"]
    M["manifests/{name}.toml"]
    S["specs/{name}.json"]
  end
  MR["ManifestRegistry::load"]
  OAP["openapi::load_and_register"]
  IDX["tool_index provider:operationId"]
  M --> MR
  S --> OAP
  MR --> OAP
  OAP --> IDX
```

On load, `openapi_spec` resolves relative to `~/.ati/specs/`. Each operation becomes a `Tool` with `endpoint` = path template, `method` from the spec, and `input_schema` built by `build_input_schema_with_locations`. Failures to read or parse the spec log a warning and leave the provider with zero tools (graceful degradation).

## `x-ati-param-location` routing

OpenAPI import does not store location metadata in the raw JSON on disk. Metadata is injected when tools are built:

| OpenAPI parameter `in` | Schema property metadata |
|------------------------|---------------------------|
| `path` | `"x-ati-param-location": "path"` |
| `query` | `"x-ati-param-location": "query"` (+ optional `x-ati-collection-format` for arrays) |
| `header` | `"x-ati-param-location": "header"` |
| `cookie` | treated as `query` |
| Request body properties | `"x-ati-param-location": "body"` |

Additional schema keys:

- `x-ati-body-encoding: "form"` when the body uses `application/x-www-form-urlencoded`
- `x-ati-collection-format`: `multi`, `csv`, `ssv`, or `pipes` for array query parameters (from OpenAPI `style` / `explode`)

At call time, `http::classify_params` reads these properties. If **no** property has `x-ati-param-location`, ATI falls back to **legacy** routing: GET/DELETE → all args as query string; POST/PUT → JSON body. Hand-written HTTP manifests use legacy mode unless you add the extension yourself.

```text
ati run finnhub:quote --symbol AAPL
        │
        ▼
classify_params (path / query / header / body)
        │
        ├─ path  → substitute {symbol} in /quote/...
        ├─ query → ?symbol=AAPL  (+ auth query param from keyring)
        ├─ header→ request headers
        └─ body  → JSON or form body
```

Path values are percent-encoded; values containing `..`, `?`, `#`, or NUL are rejected.

## Per-operation overrides

After import, tune individual tools under `[provider.openapi_overrides.<operationId>]`:

- `hint`, `tags`, `description`, `scope`
- `response_extract`, `response_format` (`markdown_table`, `json`, `raw`)

Overrides merge at `to_ati_tool` time; they do not change HTTP routing.

## Keyring key hints

| Situation | Key name | CLI |
|-----------|----------|-----|
| Default import with auth | `{name}_api_key` | `ati key set {name}_api_key <secret>` |
| Custom name | `--auth-key my_key` | `ati key set my_key <secret>` |
| Query auth (Finnhub) | value injected as `auth_query_name` (e.g. `token`) | same keyring entry as `auth_key_name` |
| Header auth | value sent as `auth_header_name` | same keyring entry |
| No auth (`crossref`) | no `auth_key_name` in manifest | none |

`ati provider load` checks the keyring and returns `needs_auth` with `setup_commands` when the key is missing.

## Troubleshooting

| Symptom | Likely cause | Action |
|---------|--------------|--------|
| `URL specs must be downloaded first with ati provider import-openapi` | Manifest references a URL, not a local file | Run import, or set `openapi_spec` to `{name}.json` under `~/.ati/specs/` |
| Provider listed, zero tools | Spec missing or parse error at load | Confirm `~/.ati/specs/{name}.json` exists; check logs with `--verbose` |
| `SSRF protection` on import | URL targets private/reserved space | Use a public HTTPS spec URL or a local file |
| Redirect error on download | Spec URL returns 3xx | Fetch the final URL yourself and import the file |
| Too many tools | Large spec | Add `openapi_max_operations`, tag filters, or operation include/exclude lists to the manifest |
| PATCH behaves like PUT | By design | Expect PUT semantics for OpenAPI PATCH operations |
| File-upload operations missing | `multipart/form-data` skipped | Use a hand-written HTTP tool or a different endpoint |

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
Import a no-auth provider, store a key, and run your first `ati run`.
</Card>
<Card title="Providers and handlers" href="/providers-and-handlers">
How `openapi` fits alongside `http`, `mcp`, and other handlers.
</Card>
<Card title="Manifest reference" href="/manifest-reference">
Full OpenAPI provider TOML fields, overrides, and validation.
</Card>
<Card title="OpenAPI stock research workflow" href="/openapi-stock-research">
End-to-end Finnhub recipe after import.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
`ati key set` and scoped tokens for proxy mode.
</Card>
</CardGroup>

---

## 09. Add MCP providers

> `ati provider add-mcp` for stdio and HTTP transports, `${key}` env injection, MCP `tools/list` discovery, and namespaced tool calls (`provider:tool`).

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/09-add-mcp-providers.md
- Generated: 2026-06-02T03:16:11.054Z

### Source Files

- `src/cli/provider.rs`
- `src/core/mcp_client.rs`
- `manifests/github-mcp.toml`
- `manifests/deepwiki-mcp.toml`
- `tests/mcp_live_test.rs`
- `src/proxy/server.rs`

---
title: "Add MCP providers"
description: "`ati provider add-mcp` for stdio and HTTP transports, `${key}` env injection, MCP `tools/list` discovery, and namespaced tool calls (`provider:tool`)."
---

MCP providers are declared with `handler = "mcp"` in `~/.ati/manifests/<name>.toml` (or via `ati provider add-mcp`), connect through `core/mcp_client.rs` over **stdio** (subprocess) or **Streamable HTTP**, discover tools with MCP `tools/list`, and expose them to agents as `provider:tool` names that `ati run` dispatches via `tools/call` after stripping the provider prefix.

## When to use MCP providers

| Use MCP when | Prefer HTTP/OpenAPI when |
|---|---|
| The upstream ships an official MCP server (`npx`, remote HTTP endpoint) | You have a REST/OpenAPI spec and want `import-openapi` |
| Tools change with the server version and should not be hand-authored | You need curated `[[tools]]`, response `extract`, or fixed schemas |
| Auth is env-based (stdio) or header-based (HTTP) on the MCP wire | Auth maps cleanly to query/header/bearer on REST |

MCP manifests do **not** include `[[tools]]`. Tool definitions come from the server at runtime.

## Add a permanent manifest

`ati provider add-mcp <name>` writes `~/.ati/manifests/<name>.toml` with `handler = "mcp"`. The command validates transport-specific fields and auth before saving; it refuses to overwrite an existing manifest.

<Steps>
<Step title="Generate the manifest">

<CodeGroup>
```bash title="HTTP (remote MCP)"
ati provider add-mcp deepwiki \
  --transport http \
  --url https://mcp.deepwiki.com/mcp \
  --description "DeepWiki documentation MCP" \
  --category documentation
```

```bash title="Stdio (subprocess MCP)"
ati provider add-mcp github \
  --transport stdio \
  --command npx \
  --args -y \
  --args @modelcontextprotocol/server-github \
  --env GITHUB_PERSONAL_ACCESS_TOKEN=${github_token} \
  --category developer-tools
```

```bash title="HTTP with bearer auth"
ati provider add-mcp parallel \
  --transport http \
  --url https://search-mcp.parallel.ai/mcp \
  --auth bearer \
  --auth-key parallel_api_key
```
</CodeGroup>

</Step>
<Step title="Store secrets in the keyring">

For `--env KEY=${key_name}` or `--auth-key`, add values before the first call:

```bash
ati key set github_token ghp_...
ati key set parallel_api_key lin_api_...
```

`add-mcp` logs a reminder when an `auth_key` is set. Stdio `${...}` placeholders in `mcp_env` are **not** auto-hinted the way CLI `@{...}` credential files are.

</Step>
<Step title="Verify discovery and run a tool">

```bash
ati provider list          # TYPE column shows mcp/http or mcp/stdio, TOOLS = auto
ati tool list              # after discovery: github:search_repositories, deepwiki:ask_question, ...
ati tool info github:search_repositories
ati run github:search_repositories --query "ati agent tools"
```

</Step>
</Steps>

### CLI flags

| Flag | Required | Purpose |
|---|---|---|
| `--transport` | Yes | `stdio` or `http` |
| `--url` | HTTP only | `mcp_url` for Streamable HTTP |
| `--command` | Stdio only | Executable (e.g. `npx`, `uvx`) |
| `--args` | No | Repeatable argv entries → `mcp_args` |
| `--env` | No | Repeatable `KEY=VALUE` → `[provider.mcp_env]`; use `VALUE=${keyring_key}` |
| `--auth` | No | `none` (default), `bearer`, or `header` |
| `--auth-key` | If bearer/header | Keyring name for HTTP auth header |
| `--auth-header` | Header auth | Custom header name (default `Authorization` at runtime) |
| `--description`, `--category` | No | Metadata for listings |

<Warning>
`--url` is mandatory for `http`; `--command` is mandatory for `stdio`. Unknown `--transport` or `--auth` values fail at CLI validation time.
</Warning>

## Manifest schema (MCP)

Generated and hand-edited MCP manifests share the same `[provider]` shape:

```toml
[provider]
name = "github"
description = "GitHub via official MCP server"
handler = "mcp"
mcp_transport = "stdio"    # or "http"
mcp_command = "npx"        # stdio only
mcp_args = ["-y", "@modelcontextprotocol/server-github"]
# mcp_url = "https://mcp.example.com/mcp"   # http only
auth_type = "none"         # none | bearer | header (HTTP)
category = "developer-tools"

[provider.mcp_env]
GITHUB_PERSONAL_ACCESS_TOKEN = "${github_token}"
```

Stock examples in the repo:

- **Stdio + env injection:** `manifests/github-mcp.toml` — `npx` launches `@modelcontextprotocol/server-github`, token via `${github_token}`.
- **HTTP, no auth:** `manifests/deepwiki-mcp.toml` — `mcp_url = "https://mcp.deepwiki.com/mcp"`.

Optional HTTP-only field `mcp_url_env` lets a proxy honor sandbox `X-Ati-Upstream-Url` after allowlist validation; it requires `mcp_transport = "http"`.

## Transports

```mermaid
flowchart TB
  subgraph cli["ati run / ati proxy"]
    R[ManifestRegistry]
    C[McpClient]
  end
  subgraph stdio["stdio transport"]
    SP[Subprocess mcp_command + mcp_args]
    ND[Newline-delimited JSON-RPC on stdin/stdout]
  end
  subgraph http["http transport"]
    POST[POST Streamable HTTP]
    SSE[JSON or text/event-stream response]
    SID[Mcp-Session-Id]
  end
  R --> C
  C -->|mcp_transport = stdio| SP --> ND
  C -->|mcp_transport = http| POST --> SSE
  POST --> SID
```

### Stdio

- Spawns `mcp_command` with `mcp_args`, clears the environment, then injects `PATH`, `HOME`, and resolved `[provider.mcp_env]`.
- JSON-RPC messages are **one JSON object per line** on stdin/stdout.
- On disconnect, stdin is closed and the child is killed to avoid orphans.
- In **proxy mode**, the proxy host runs the subprocess with real credentials; the sandbox client never sees tokens in `mcp_env`.

### HTTP (Streamable HTTP)

- `POST` to `mcp_url` with `Accept: application/json, text/event-stream`.
- Responses may be single JSON bodies or SSE streams (`data:` lines parsed into JSON-RPC).
- `Mcp-Session-Id` from responses is stored and sent on later requests; session teardown uses `DELETE` when a session id exists.
- `auth_type = bearer` → `Authorization: Bearer <keyring>`; `header` uses `auth_header_name` (default `Authorization`).
- `${key}` placeholders in `mcp_url` are resolved from the keyring the same way as env values.

Protocol revision used at initialize: **`2025-03-26`**. The client sends `initialize`, requires a `tools` capability, then `notifications/initialized`.

## Tool discovery and naming

Discovery flow:

1. **Proxy startup:** `discover_all_mcp_tools` connects to each MCP provider (30s timeout per provider, up to 10 concurrent), calls `tools/list` (with pagination/cursor), registers tools, then disconnects.
2. **Lazy CLI:** If `ati run <tool>` misses the static index but the prefix matches an MCP provider name, ATI connects once, lists tools, registers them, then resolves the call.
3. **`ati provider load --mcp`:** Writes a TTL cache under `~/.ati/cache/providers/` and optionally probes with `tools/list` (JSON output includes tool names when the probe succeeds).

Registration prefixes every MCP tool name:

```
<provider_name>:<mcp_tool_name>
```

Examples: `github:search_repositories`, `deepwiki:ask_question`. Scope entries are auto-set to `tool:<prefixed_name>`.

`tools/list` results are cached inside `McpClient` for the lifetime of the connection. Pagination follows `nextCursor` (cap 10_000 tools).

## Executing `provider:tool` calls

```mermaid
sequenceDiagram
  participant Agent as Agent / ati run
  participant ATI as ATI dispatch
  participant MCP as MCP server
  Agent->>ATI: ati run github:search_repositories --query ati
  ATI->>ATI: Scope check tool:github:search_repositories
  ATI->>MCP: connect → initialize
  ATI->>MCP: tools/call name=search_repositories
  Note over ATI,MCP: Prefix github: stripped before tools/call
  MCP-->>ATI: content[] (text/json/image)
  ATI-->>Agent: formatted text/json output
  ATI->>MCP: disconnect
```

Dispatch (`mcp_client::execute_with_gen`):

1. Connect (resolve `${key}` in env and URL; optional dynamic `auth_generator`).
2. Strip `provider:` prefix — `github:read_file` → `read_file`.
3. `tools/call` with the bare MCP tool name and normalized args.
4. Map `McpToolResult` to JSON (single text item parsed as JSON when possible).
5. Disconnect.

Proxy JSON-RPC (`POST /mcp`) exposes `tools/list` and `tools/call` over the **already-discovered** registry, with JWT scope checks on each tool. MCP tool names in `tools/call` use the full `provider:tool` form.

## Ephemeral load vs permanent add

| Command | Persistence | Discovery probe |
|---|---|---|
| `ati provider add-mcp` | `~/.ati/manifests/<name>.toml` | No (manifest only) |
| `ati provider load --mcp ...` | Cache JSON with TTL (default 1h) | Yes — connect, `tools/list`, disconnect |
| `ati provider load --mcp ... --save` | Delegates to `add-mcp` | N/A |

Example ephemeral HTTP load:

```bash
ati provider load --mcp --name serpapi \
  --transport http --url https://mcp.serpapi.com/mcp \
  --output json
```

JSON may include `status`, `tools_count`, `tools`, `probe`, and `setup_commands` when keyring refs are missing.

## Credential patterns

| Pattern | Where | Resolution |
|---|---|---|
| `${key_name}` | `[provider.mcp_env]`, `mcp_url` | Keyring lookup; missing key leaves placeholder unchanged in strings |
| `--auth bearer` + `--auth-key` | HTTP `auth_type` | `Authorization: Bearer <keyring>` |
| `--auth header` + `--auth-key` + `--auth-header` | HTTP custom header | Header name + keyring value |
| `auth_generator` | Advanced manifests | Dynamic token/env injection (`ATI_AUTH_TOKEN`, `extra_env`) |

<Note>
MCP env resolution supports `${key}` (including inline `prefix/${key}/suffix`). The `@{key}` credential-file syntax applies to **CLI** providers, not MCP `mcp_env`.
</Note>

## Proxy vs local execution

| Mode | Who runs MCP | Credentials |
|---|---|---|
| Local (default) | `ati` on the agent machine | Keyring decrypted via session key |
| Proxy (`ATI_PROXY_URL`) | Proxy host | Keyring on proxy; sandbox sends JWT only |

In proxy mode, stdio MCP servers start on the proxy with resolved secrets. HTTP MCP calls originate from the proxy with bearer/header auth from the proxy keyring.

## Troubleshooting

| Symptom | Likely cause | What to check |
|---|---|---|
| `Unknown tool: 'github:foo'` | Discovery not run or typo | `ati tool list`; ensure provider prefix matches manifest `name` |
| `Failed to spawn MCP server` | Bad `--command` / missing `npx` | PATH; run command manually |
| `MCP server did not return tools capability` | Non-compliant server | Server must answer `initialize` with `capabilities.tools` |
| `MCP tool discovery timed out (30s)` | Slow stdio cold start | Retry; reduce providers probed at proxy boot |
| HTTP 401/403 | Missing bearer/header key | `ati key set <auth_key_name> ...` |
| Env var empty in subprocess | Keyring miss | `ati key set` for each `${...}` in `mcp_env` |
| Suggestions list on typo | Wrong suffix after `provider:` | Error lists tools sharing the same provider prefix |

Live integration tests (`tests/mcp_live_test.rs`, `tests/mcp_cmd_test.rs`) cover HTTP/stdio manifest generation, env injection, and real GitHub/Linear servers — run with `cargo test --test mcp_live_test -- --ignored` when API keys and `npx` are available.

## Related pages

<CardGroup>
<Card title="Providers and handlers" href="/providers-and-handlers">
Handler types, dispatch paths, and how MCP fits beside HTTP, OpenAPI, and CLI.
</Card>
<Card title="Manifest reference" href="/manifest-reference">
Full `[provider]` TOML fields including MCP, auth, and validation errors.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
Keyring setup, `ati key set`, and scoped tokens for proxy mode.
</Card>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
Run `ati proxy`, startup MCP discovery, and `/mcp` JSON-RPC for sandboxes.
</Card>
<Card title="Build, test, and troubleshooting" href="/build-test-and-troubleshooting">
`cargo test`, live MCP tests, and E2E scripts.
</Card>
</CardGroup>

---

## 10. CLI and HTTP manifests

> `ati provider add-cli`, hand-written `[[tools]]` HTTP manifests, `${key}` vs `@{key}` credential files, `cli_output_args` binary capture, and curated subprocess environments.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/10-cli-and-http-manifests.md
- Generated: 2026-06-02T03:16:25.995Z

### Source Files

- `src/cli/provider.rs`
- `src/core/cli_executor.rs`
- `src/cli/cli_capture.rs`
- `manifests/google-workspace.toml`
- `manifests/hackernews.toml`
- `manifests/README.md`
- `src/core/http.rs`

---
title: "CLI and HTTP manifests"
description: "`ati provider add-cli`, hand-written `[[tools]]` HTTP manifests, `${key}` vs `@{key}` credential files, `cli_output_args` binary capture, and curated subprocess environments."
---

Hand-written TOML under `~/.ati/manifests/` registers two complementary provider shapes: **HTTP** manifests (`handler` omitted or `http`) declare one or more `[[tools]]` with `base_url`, `endpoint`, and JSON Schema args; **CLI** manifests (`handler = "cli"`) wrap a local binary, inject secrets through `[provider.cli_env]`, and optionally declare output-file capture for proxy-mode sandboxes. `ati provider add-cli` scaffolds CLI manifests; HTTP manifests are edited directly or copied from shipped examples such as `manifests/hackernews.toml`.

## When to use which handler

| Handler | Manifest shape | Tool discovery | Typical invocation |
|---------|----------------|----------------|-------------------|
| `http` (default) | `[provider]` + `[[tools]]` | Each `[[tools]].name` is indexed | `ati run hackernews_top_stories` with `--key value` args |
| `cli` | `[provider]` only (optional empty `[[tools]]`) | Auto-registers one tool named after `provider.name` | `ati run gh -- pr list` — everything after the tool name is forwarded as CLI argv |

<Note>
CLI providers with no `[[tools]]` section still appear in `ati tool list`: the registry synthesizes a single tool whose name equals `provider.name` and whose scope defaults to `tool:<name>`.
</Note>

## Register a CLI provider with `ati provider add-cli`

`ati provider add-cli` writes `~/.ati/manifests/<name>.toml` and refuses to overwrite an existing file.

```bash
ati provider add-cli gh --command gh \
  --env 'GH_TOKEN=${github_token}'

ati provider add-cli gcloud --command gcloud \
  --default-args "--format" --default-args "json" \
  --env 'GOOGLE_APPLICATION_CREDENTIALS=@{gcp_service_account}' \
  --timeout 90 \
  --description "Google Cloud SDK" \
  --category cloud
```

<ParamField body="name" type="string" required>
Provider name; becomes the implicit tool name for `ati run <name>`.
</ParamField>

<ParamField body="--command" type="string" required>
Binary path or name resolved via `PATH` (`cli_command` in the manifest).
</ParamField>

<ParamField body="--default-args" type="string[]">
Prepended to every invocation (`cli_default_args`).
</ParamField>

<ParamField body="--env" type="KEY=VALUE[]">
Entries stored under `[provider.cli_env]`. Use `${key}` or `@{key}` forms (see below).
</ParamField>

<ParamField body="--timeout" type="u64">
Sets `cli_timeout_secs` (default **120** when omitted).
</ParamField>

Generated manifests always set `auth_type = "none"` at the provider level; authentication is handled via `cli_env` and the keyring, not HTTP `auth_type` fields.

### Example: Google Workspace CLI

The shipped `google-workspace.toml` pattern wraps `gws` with a file-materialized service account:

```toml
[provider]
name = "google_workspace"
handler = "cli"
cli_command = "gws"
cli_timeout_secs = 120
auth_type = "none"

[provider.cli_env]
GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE = "@{google_workspace_credentials}"
# GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER = "${google_workspace_user}"
```

```bash
ati key set google_workspace_credentials "$(cat service-account.json)"
ati run google_workspace -- auth setup   # subcommands pass through verbatim
```

## Hand-written HTTP manifests

HTTP providers declare `base_url`, `auth_type`, and one or more tools. The Hacker News manifest is a minimal no-auth example:

```toml
[provider]
name = "hackernews"
description = "Hacker News — Top tech stories"
base_url = "https://hacker-news.firebaseio.com/v0"
auth_type = "none"

[[tools]]
name = "hackernews_top_stories"
description = "Get IDs of the current top 500 stories"
endpoint = "/topstories.json"
method = "GET"
scope = "tool:hackernews_stories"

[tools.input_schema]
type = "object"

[tools.response]
format = "json"
```

### Provider auth fields (HTTP)

| `auth_type` | Keyring / manifest fields | Request injection |
|-------------|---------------------------|-------------------|
| `none` | — | No credentials |
| `bearer` | `auth_key_name` | `Authorization: Bearer <key>` |
| `header` | `auth_key_name`, optional `auth_header_name` | Custom header (default `X-Api-Key`) |
| `query` | `auth_key_name`, `auth_query_name` | Query parameter |
| `basic` | `auth_key_name` | HTTP Basic |
| `oauth2` | OAuth fields on `[provider]` | Token exchange with in-process cache |

Store secrets with `ati key set <auth_key_name> <value>` before calling tools.

### Parameter routing

Hand-written HTTP tools without `x-ati-param-location` in `tools.input_schema` use **legacy mode**: `GET` sends all args as query parameters; `POST`, `PUT`, and `DELETE` send all args as a JSON body. OpenAPI-imported tools use location metadata instead (`path`, `query`, `header`, `body`). See the import OpenAPI page for that path.

### Tool metadata

Each `[[tools]]` entry supports `description`, optional per-tool `scope` (auto-assigned as `tool:<name>` when omitted on non-internal providers), `input_schema`, and `[tools.response]` with `format` and optional `extract` for response shaping.

## `${key}` vs `@{key}` in `cli_env`

Both forms read from the AES-256-GCM keyring at execution time. They differ in how the secret reaches the subprocess.

| Syntax | Resolution | Use when |
|--------|------------|----------|
| `${key_ref}` | Inline string substitution anywhere in the value | Tokens, project IDs, short secrets (`GH_TOKEN=${github_token}`) |
| `@{key_ref}` | Writes keyring content to `~/.ati/.creds/<key_ref>` (mode `0600`), sets env var to the **file path** | CLIs that require a credentials **file** (`GOOGLE_APPLICATION_CREDENTIALS`, Workspace CLI, etc.) |

<Warning>
`@{key}` files are wiped on drop when the keyring is **ephemeral** (production/proxy session). In dev mode (`wipe_on_drop = false`), the stable path `~/.ati/.creds/<key_ref>` is reused across runs.
</Warning>

`add-cli` logs hints when env values reference `${...}` or `@{...}` so you remember `ati key set`.

Plain strings in `cli_env` pass through unchanged. `auth_generator` on the provider (optional) can also inject `ATI_AUTH_TOKEN` and extra env vars before spawn.

## Curated subprocess environment

CLI execution does **not** inherit the full host environment. `cli_executor` builds a minimal allowlist, then layers resolved `cli_env`:

| Always forwarded (if set on host) | From manifest |
|-----------------------------------|---------------|
| `PATH`, `HOME`, `TMPDIR`, `LANG`, `USER`, `TERM` | All resolved `[provider.cli_env]` entries |

The subprocess is spawned with `.env_clear()` then `.envs(&final_env)`, so undeclared variables are absent unless the CLI reads them from its own config files.

Invocation shape:

```text
<cli_command> <cli_default_args...> <caller argv from ati run...>
```

Default timeout is **120** seconds (`cli_timeout_secs`). Failures surface as typed errors: spawn failure, non-zero exit (stderr included), or timeout.

## Invoking CLI tools from `ati run`

`ati run` uses `trailing_var_arg` so native CLI flags pass through:

```bash
ati run gh -- pr list --state open --limit 5
ati run google_workspace -- drive files list --params '{"pageSize": 5}'
```

- Everything after the tool name becomes **raw argv** for `handler = "cli"`.
- HTTP tools use `--key value` pairs parsed into a map (plus schema normalization).
- In proxy mode, the client sends both the parsed map and `raw_args` so the proxy can call `args_as_positional()` for CLI tools without losing bare words like `browse` or `status`.

Stdout is parsed as JSON when possible; otherwise the trimmed string is returned. That legacy shape applies when no output capture is configured.

## Binary output capture (`cli_output_args` / `cli_output_positional`)

CLIs that write binaries to disk (screenshots, PDFs, downloads) need explicit capture in **proxy mode**: otherwise bytes remain on the proxy filesystem while the sandbox agent supplied a path it expects locally.

Add to the CLI `[provider]` block:

```toml
cli_output_args = ["--output", "-o", "--out"]

[provider.cli_output_positional]
"browse screenshot" = 0
"browse pdf" = 0
```

| Field | Behavior |
|-------|----------|
| `cli_output_args` | Named flags whose **value** is an output path. Supports `--flag value` and `--flag=value`. Case-insensitive flag match. |
| `cli_output_positional` | Map of subcommand prefix (whitespace-separated) → 0-based index among positional args **after** the matched prefix. Longest matching prefix wins. Slots already rewritten by named flags are skipped (no double capture). |

```mermaid
sequenceDiagram
    participant Agent as Sandbox agent
    participant CLI as ati CLI
    participant Proxy as ati proxy
    participant Sub as Wrapped binary

    Agent->>CLI: ati run bb browse screenshot /tmp/shot.png
    CLI->>Proxy: POST /call raw_args verbatim
    Proxy->>Proxy: apply_output_captures → temp path
    Proxy->>Sub: bb browse screenshot /tmp/.ati-cli-out-*.png
    Sub-->>Proxy: writes PNG to temp
    Proxy->>Proxy: collect_capture_results base64 envelope
    Proxy-->>CLI: stdout + outputs{path: content_base64}
    CLI->>CLI: materialize_outputs → write /tmp/shot.png
    CLI-->>Agent: strip base64, set outputs[].path
```

When capture is active, the JSON response includes `stdout` plus an `outputs` object keyed by the **agent's original paths**. Each entry carries `content_base64`, `size_bytes`, and guessed `content_type` until the sandbox CLI decodes and writes the file.

<Info>
Manifests without `cli_output_args` / `cli_output_positional` keep the previous flat stdout-only response. Multiple output flags in one invocation are all captured.
</Info>

### Safety and limits

| Control | Default / behavior |
|---------|-------------------|
| `ATI_CLI_MAX_OUTPUT_BYTES` | **500 MB** per captured file |
| Missing output file after success | `OutputMissing` error |
| Oversize file | `OutputTooLarge` error |
| Subprocess failure / timeout | Temp capture files discarded |
| Temp path naming | Preserves extension from the agent path (`.png`, `.pdf`, …) |

Proxy `/call` returns the envelope with base64 intact; only the sandbox-side `materialize_outputs` step writes disk and removes `content_base64` from the formatted response.

## HTTP vs CLI dispatch (local and proxy)

```text
ati run <tool_name> [args...]
        │
        ├─ handler "mcp"     → mcp_client (tools/namespaced)
        ├─ handler "cli"     → cli_executor(raw_args) → optional materialize_outputs
        └─ default/http      → core/http execute_tool (legacy or classified params)
```

Proxy mode routes the same handlers on `POST /call`; CLI uses positional args from `raw_args` / `args` array, not the HTTP args map.

## Verification checklist

<Steps>
<Step title="Load manifests">
Run `ati provider list` and `ati tool list` after adding or editing TOML under `~/.ati/manifests/`.
</Step>
<Step title="HTTP smoke test">
```bash
ati run hackernews_top_stories -J
```
Expect a JSON array of numeric story IDs.
</Step>
<Step title="CLI smoke test">
```bash
ati provider add-cli myecho --command echo
ati run myecho -- hello
```
Expect `hello` on stdout.
</Step>
<Step title="Credential file path">
For `@{key}` env vars, confirm `ati key set` was run and the subprocess receives a readable path under `~/.ati/.creds/`.
</Step>
<Step title="Output capture (proxy)">
With `ATI_PROXY_URL` set and `cli_output_args` configured, run a tool that writes a file and confirm the path exists inside the sandbox after the call completes.
</Step>
</Steps>

## Common failures

| Symptom | Likely cause |
|---------|----------------|
| `Unknown tool: 'gh'` | Manifest missing or provider name mismatch; CLI tool name equals `provider.name`, not `gh:subcommand`. |
| `Missing keyring key: github_token` | `${...}` reference without `ati key set`. |
| `CLI exited with code N` | Upstream CLI error; stderr is included in the error. |
| `Captured output exceeds ATI_CLI_MAX_OUTPUT_BYTES` | Raise env cap or reduce output size. |
| `Captured output was not produced` | CLI did not write to the substituted temp path; check `cli_output_args` / positional rules. |
| HTTP 401/403 | Wrong `auth_type` or missing `auth_key_name` value in keyring. |

## Related pages

<CardGroup>
<Card title="Providers and handlers" href="/providers-and-handlers">
Handler matrix, `provider:tool` naming, and dispatch modules.
</Card>
<Card title="Manifest reference" href="/manifest-reference">
Full TOML schema for `[provider]` and `[[tools]]` fields.
</Card>
<Card title="Add MCP providers" href="/add-mcp-providers">
Auto-discovered tools via MCP instead of argv passthrough.
</Card>
<Card title="Import OpenAPI" href="/import-openapi">
Generate HTTP tools from a spec instead of hand-written `[[tools]]`.
</Card>
<Card title="Execution modes" href="/execution-modes">
Keyring, proxy routing, and where credentials live at runtime.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
`ati key set` and scoped tokens for production sandboxes.
</Card>
</CardGroup>

---

## 11. Deploy proxy server

> Run `ati proxy`, bind/port/`--env-keys`, optional `--features db` persistence, passthrough and HMAC sig-verify flags, VM/systemd examples, and health/JWKS probes.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/11-deploy-proxy-server.md
- Generated: 2026-06-02T03:16:14.451Z

### Source Files

- `src/proxy/server.rs`
- `src/main.rs`
- `deploy/examples/vm/README.md`
- `deploy/examples/vm/systemd/ati.service`
- `docs/PERSISTENCE.md`
- `src/core/passthrough.rs`
- `src/core/sig_verify.rs`

---
title: "Deploy proxy server"
description: "Run `ati proxy`, bind/port/`--env-keys`, optional `--features db` persistence, passthrough and HMAC sig-verify flags, VM/systemd examples, and health/JWKS probes."
---

`ati proxy` starts the same `ati` binary as an Axum HTTP server that holds provider credentials, loads manifests from `<ati_dir>/manifests`, validates JWT or DB-backed virtual keys on named routes, and optionally fronts raw upstream HTTP via passthrough manifests with HMAC sandbox signature verification.

```mermaid
flowchart LR
  subgraph sandbox["Agent sandbox"]
    CLI["ati run / SDK"]
  end
  subgraph proxy["ati proxy"]
    SIG["sig_verify middleware"]
    AUTH["auth middleware"]
    NAMED["/call /mcp /skills /help …"]
    PT["passthrough fallback"]
    KR["Keyring"]
  end
  subgraph upstream["Upstream APIs"]
    API["HTTP / MCP / OpenAPI"]
    EDGE["Passthrough upstreams"]
  end
  CLI -->|"Bearer JWT or Ati-Key"| SIG
  SIG --> AUTH
  AUTH --> NAMED
  AUTH --> PT
  NAMED --> KR
  PT --> KR
  KR --> API
  KR --> EDGE
```

## Prerequisites

| Requirement | Notes |
|---|---|
| Built `ati` binary | Release: `cargo build --release` or musl artifact from GitHub releases |
| ATI directory | `manifests/`, optional `keyring.enc` or `credentials`, optional `skills/` |
| JWT validation keys (production) | `ATI_JWT_PUBLIC_KEY` (ES256) or `ATI_JWT_SECRET` (HS256) on the proxy host |
| Sandbox routing | Set `ATI_PROXY_URL` and `ATI_SESSION_TOKEN` in agent environments |

For a first local smoke test without TLS or passthrough:

```bash
ati init --proxy
ati proxy --port 8090 --ati-dir ~/.ati
```

Default listen address is `127.0.0.1:8090` when `--bind` is omitted.

## Command reference

```bash
ati proxy [OPTIONS]
```

| Flag | Default | Purpose |
|---|---|---|
| `--port` | `8090` | TCP port |
| `--bind` | `127.0.0.1` | Bind address; use `0.0.0.0` for all interfaces |
| `--ati-dir` | `~/.ati` (or `ATI_DIR`) | Manifests, keyring, skills root |
| `--env-keys` | off | Load credentials from `ATI_KEY_*` env vars instead of disk keyring |
| `--migrate` | off | Apply embedded Postgres migrations when `ATI_DB_URL` is set (requires `db` feature build) |
| `--enable-passthrough` | off | Compile and serve `handler = "passthrough"` routes |
| `--sig-verify-mode` | `log` | HMAC verification: `log`, `warn`, or `enforce` |
| `--sig-drift-seconds` | `60` | Max clock skew for signature timestamp |
| `--sig-exempt-paths` | built-in defaults | Comma-separated path globs exempt from HMAC check |

<ParamField body="--env-keys" type="boolean">
When set, the proxy builds the keyring from every non-empty environment variable prefixed with `ATI_KEY_`. The prefix is stripped and the remainder is lowercased to form the keyring entry name (for example `ATI_KEY_FINNHUB_API_KEY` → `finnhub_api_key`). No `keyring.enc` read occurs.
</ParamField>

<ParamField body="--enable-passthrough" type="boolean">
Enables manifest-driven raw HTTP/WebSocket reverse proxying on the router fallback. Passthrough routes skip JWT auth and rely on HMAC sig-verify (and upstream credential injection from the keyring). Without this flag, unmatched paths return 404.
</ParamField>

<ParamField body="--sig-verify-mode" type="enum">
`log` — always allow; structured-log validity (safe rollout default). `warn` — allow; set `X-Signature-Status` on responses. `enforce` — reject invalid or missing signatures with `403`; startup fails if `sandbox_signing_shared_secret` is absent in enforce mode.
</ParamField>

Production edge deployments often bind loopback and terminate TLS in front:

```bash
ati proxy \
  --bind 127.0.0.1 \
  --port 8080 \
  --ati-dir /var/lib/ati \
  --enable-passthrough \
  --sig-verify-mode enforce \
  --sig-drift-seconds 60
```

<Warning>
Do not expose `--enable-passthrough` on a public interface with `--sig-verify-mode log` or `warn` and no signing secret configured. Passthrough has no JWT layer; non-blocking sig-verify modes leave those routes effectively unauthenticated.
</Warning>

## ATI directory layout

The proxy resolves manifests at `<ati_dir>/manifests/*.toml` and skills at `<ati_dir>/skills/`. With `--ati-dir /var/lib/ati`, manifests belong at `/var/lib/ati/manifests/` (not under `/etc/ati` unless that path is your `ati_dir`).

| Path | Role |
|---|---|
| `<ati_dir>/manifests/` | Provider and tool definitions |
| `<ati_dir>/keyring.enc` | Encrypted credentials (preferred) |
| `<ati_dir>/credentials` | Plaintext fallback if no keyring |
| `<ati_dir>/skills/` | Local skill registry |

Keyring load order without `--env-keys`:

1. Decrypt `keyring.enc` with a one-shot session key (sealed mode)
2. Else decrypt with persistent local key material
3. Else load `credentials` plaintext file
4. Else empty keyring (authenticated tools fail at runtime)

Send `SIGHUP` after `ati edge rotate-keyring` to hot-reload the keyring and signing secret without restarting the process.

## Credential modes

<Tabs>
<Tab title="Encrypted keyring">

Standard production path: populate `keyring.enc` via `ati key set` or `ati edge bootstrap-keyring`, restrict ownership (`ati:ati`, mode `0600`), run the proxy without `--env-keys`.

</Tab>
<Tab title="Environment keys">

Container-friendly mode:

```bash
export ATI_KEY_FINNHUB_API_KEY="..."
export ATI_KEY_SANDBOX_SIGNING_SHARED_SECRET="..."   # HMAC secret for sig-verify
ati proxy --env-keys --port 8090
```

Signing secret keyring name remains `sandbox_signing_shared_secret` (env: `ATI_KEY_SANDBOX_SIGNING_SHARED_SECRET`).

</Tab>
</Tabs>

## Authentication on named routes

Named ATI routes (`/call`, `/mcp`, `/help`, `/tools`, `/skills`, `/skillati/*`, …) pass through `auth_middleware` after sig-verify (when not exempt).

| Credential | Header | Behavior |
|---|---|---|
| JWT | `Authorization: Bearer <jwt>` | Validated against `ATI_JWT_PUBLIC_KEY` or `ATI_JWT_SECRET`; scopes enforced per handler |
| Virtual key | `Authorization: Ati-Key <raw>` | DB lookup when persistence is connected; synthesizes scope claims |
| Dev (no JWT config) | none | Requests allowed unless an `Ati-Key` header is present |

Public endpoints bypass auth: `/health`, `/.well-known/jwks.json`.

Passthrough-matched requests skip JWT and use HMAC verification instead.

Configure signing keys and token issuance on the proxy host before production traffic — see the JWT configuration page in Related pages.

## HMAC sandbox signatures

Sandboxes sign outbound requests with HMAC-SHA256 over `{timestamp}.{METHOD}.{path}` using the shared secret in keyring entry `sandbox_signing_shared_secret`.

<ParamField body="X-Sandbox-Signature" type="header" required>
Format: `t=<unix_ts>,s=<hex_hmac>`. Optional `X-Sandbox-Job-Id` is logged only.
</ParamField>

Default exempt paths (overridable with `--sig-exempt-paths`):

- `/healthz`, `/health`
- `/.well-known/jwks.json`
- `/root/*`, `/npm/*`, `/otel/*` (package caches and OTel forwarding)

Rollout pattern from the VM example:

1. Start with `--sig-verify-mode log` for at least 24 hours.
2. Confirm structured logs show `valid=true` and no unexpected `valid=false`.
3. Switch to `--sig-verify-mode enforce` and restart (or reload unit).

## Passthrough manifests

Passthrough providers use `handler = "passthrough"` in TOML. At startup the proxy builds a longest-prefix router from `host_match`, `path_prefix`, `base_url`, auth injection, deny paths, and optional `forward_authorization_paths` for virtual-key upstreams.

Clone `deploy/examples/vm/manifests/example-passthrough.toml` per upstream. Enable routing only with `--enable-passthrough`.

```toml
[provider]
name = "example-service"
handler = "passthrough"
base_url = "https://api.example.com"
path_prefix = "/example"
strip_prefix = true
```

## Optional Postgres persistence (`db` feature)

Persistence is opt-in at compile time and runtime.

<CodeGroup>
```bash title="Build with db"
cargo build --release --features db --target x86_64-unknown-linux-musl
```

```bash title="Pre-built -db artifact"
curl -fsSL https://github.com/Parcha-ai/ati/releases/latest/download/ati-x86_64-unknown-linux-musl-db.tar.gz \
  | tar xz && sudo mv ati /usr/local/bin/
```
</CodeGroup>

| Variable / flag | Purpose |
|---|---|
| `ATI_DB_URL` | Postgres connection string; unset → `db: disabled` in health |
| `--migrate` | Apply embedded migrations on startup (idempotent) |
| `ATI_ADMIN_TOKEN` | Bearer for `/admin/keys/*` when DB is connected |

Startup behavior:

- `ATI_DB_URL` unset → proxy starts; persistence disabled
- `ATI_DB_URL` set, DB unreachable → proxy **exits** with error
- `ATI_DB_URL` set, binary built without `db` → proxy **exits** with rebuild hint

PR 1 schema only: tables exist; per-call audit and virtual-key writes land in later PRs. See `docs/PERSISTENCE.md` for schema and operations detail.

## VM and systemd example

`deploy/examples/vm/` documents a static-egress edge VM: Caddy terminates TLS on `:443` and reverse-proxies to `ati` on `127.0.0.1:8080`.

<Steps>
<Step title="Install binary and layout">

```bash
curl -L https://github.com/Parcha-ai/ati/releases/latest/download/ati-x86_64-unknown-linux-musl \
  -o /usr/local/bin/ati && chmod +x /usr/local/bin/ati
useradd --system --no-create-home --shell /usr/sbin/nologin ati
install -d -o ati -g ati -m 0750 /var/lib/ati/manifests
```

</Step>
<Step title="Install unit files">

Copy `deploy/examples/vm/systemd/ati.service` to `/etc/systemd/system/`. The unit runs:

```ini
ExecStart=/usr/local/bin/ati proxy \
    --bind 127.0.0.1 \
    --port 8080 \
    --ati-dir /var/lib/ati \
    --enable-passthrough \
    --sig-verify-mode log \
    --sig-drift-seconds 60
```

Hardening includes `ProtectSystem=strict`, `NoNewPrivileges=true`, and `LimitMEMLOCK=64M` for keyring `mlock`.

</Step>
<Step title="Bootstrap keyring">

```bash
sudo -u ati ati edge bootstrap-keyring \
  --vault "Production Secrets" \
  --item "ATI Edge VM Keyring" \
  --ati-dir /var/lib/ati \
  --op-token-file /etc/op-service-account-token
systemctl enable --now ati
```

</Step>
</Steps>

Optional: `systemd/ati.service.d/otel.conf.example` when built with `--features otel`; weekly keyring rotation via `ati-rotate-keyring.timer`.

## Health and JWKS probes

Use these for load balancers, systemd `ExecStartPost`, and deploy gates.

:::endpoint GET /health
Liveness and inventory snapshot. No authentication.

<ResponseField name="status" type="string">
Always `"ok"` when the handler runs.
</ResponseField>

<ResponseField name="version" type="string">
Crate version from the running binary.
</ResponseField>

<ResponseField name="tools" type="number">
Count of public tools after MCP/OpenAPI discovery at startup.
</ResponseField>

<ResponseField name="providers" type="number">
Loaded provider count.
</ResponseField>

<ResponseField name="skills" type="number">
Local skill registry count.
</ResponseField>

<ResponseField name="auth" type="string">
`"jwt"` when JWT validation is configured; `"disabled"` otherwise.
</ResponseField>

<ResponseField name="db" type="string">
`"disabled"` or `"connected"` (pool configured; not a live DB probe in PR 1).
</ResponseField>
:::

<RequestExample>
```bash
curl -fsS http://127.0.0.1:8090/health | jq
```
</RequestExample>

<ResponseExample>
```json
{
  "status": "ok",
  "version": "0.8.0-rc.7",
  "tools": 104,
  "providers": 10,
  "skills": 0,
  "auth": "jwt",
  "db": "disabled"
}
```
</ResponseExample>

:::endpoint GET /.well-known/jwks.json
JWKS document for ES256 JWT validation by sandboxes or orchestrators. Requires `ATI_JWT_PUBLIC_KEY` at proxy startup.

Returns `404` with `{"error":"JWKS not configured"}` when only HS256 secret is configured (no public key PEM).
:::

<RequestExample>
```bash
curl -fsS http://127.0.0.1:8090/.well-known/jwks.json | jq
```
</RequestExample>

<Check>
`/health` and `/.well-known/jwks.json` are exempt from JWT and default HMAC exempt lists — suitable for unauthenticated probes.
</Check>

## Connect sandboxes

Point agents at the proxy; credentials stay on the server:

```bash
export ATI_PROXY_URL="http://proxy-host:8090"
export ATI_SESSION_TOKEN="<jwt-from-ati-token-issue>"
ati run github:search_repositories --query "ati"
```

The client sends `Authorization: Bearer` on every proxy request. Scope enforcement matches local mode.

## Troubleshooting

| Symptom | Likely cause | Mitigation |
|---|---|---|
| Proxy exits at startup with enforce + passthrough | Missing `sandbox_signing_shared_secret` | Load keyring or set `ATI_KEY_SANDBOX_SIGNING_SHARED_SECRET` with `--env-keys` |
| `401` on `/call` | JWT missing, invalid, or wrong audience | Check `ATI_JWT_*` on proxy; re-issue token with matching `aud` |
| `403` on passthrough paths | Sig-verify enforce mode | Fix sandbox signer; verify clock drift within `--sig-drift-seconds` |
| Tools count `0` in `/health` | Empty or wrong `manifests/` path | Confirm `--ati-dir` and directory permissions |
| Authenticated tools fail | Empty keyring | Add keys via `ati key set` or env vars |
| `db: connected` but admin 503 | `ATI_ADMIN_TOKEN` unset | Set admin bearer for `/admin/keys/*` |
| `EAGAIN` / mlock warnings | Low `LimitMEMLOCK` | Raise systemd `LimitMEMLOCK` (VM unit uses `64M`) |

Structured startup log line includes `auth`, `keyring` source, `passthrough` route count, `sig_verify_mode`, and `sig_verify_secret` boolean for operator review.

## Related pages

<CardGroup>
<Card title="Execution modes" href="/execution-modes">
When sandboxes use `ATI_PROXY_URL` versus local keyring mode and threat-model trade-offs.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
`ati init --proxy`, ES256/HS256 env vars, and token issuance for `ATI_SESSION_TOKEN`.
</Card>
<Card title="Proxy API reference" href="/proxy-api-reference">
Full route list, request shapes, and admin endpoints.
</Card>
<Card title="Environment variables" href="/environment-variables">
`ATI_PROXY_URL`, `ATI_DB_URL`, `ATI_ADMIN_TOKEN`, JWT, and OTel variables.
</Card>
<Card title="Security and production" href="/security-and-production">
Proxy hardening, SSRF, download allowlists, and sig-verify threat model.
</Card>
<Card title="Build, test, and troubleshooting" href="/build-test-and-troubleshooting">
Musl builds, feature flags, and E2E scripts including `scripts/test_proxy_server_e2e.sh`.
</Card>
</CardGroup>

---

## 12. Configure JWT and keys

> `ati key set/list/remove`, `ati token keygen|issue|inspect|validate`, `ati init --proxy`, per-provider session token env overrides, and orchestrator token issuance patterns.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/12-configure-jwt-and-keys.md
- Generated: 2026-06-02T03:16:41.810Z

### Source Files

- `src/cli/keys.rs`
- `src/cli/token.rs`
- `src/core/jwt.rs`
- `src/core/token.rs`
- `docs/JWT_STANDARDS_2026.md`
- `ati-client/python/src/ati/token.py`

---
title: "Configure JWT and keys"
description: "`ati key set/list/remove`, `ati token keygen|issue|inspect|validate`, `ati init --proxy`, per-provider session token env overrides, and orchestrator token issuance patterns."
---

ATI separates **upstream API credentials** (provider keys in `~/.ati/credentials`, consumed by `ati key` and the proxy keyring) from **sandbox session JWTs** (scoped bearer tokens issued by `ati token issue` or an orchestrator, carried as `ATI_SESSION_TOKEN` or per-provider overrides). The proxy validates JWTs from environment-loaded signing keys (`ATI_JWT_*`); scopes live in the JWT `scope` claim, not in request bodies.

## Credential planes

| Plane | Storage | CLI / API | Consumed by |
|-------|---------|-----------|-------------|
| Upstream API keys | `~/.ati/credentials` (JSON, mode `0600`) | `ati key set/list/remove` | Local `ati run`, `ati proxy` keyring |
| Encrypted keyring (local mode) | `keyring.enc` + one-shot `/run/ati/.key` | (orchestrator / sealed file) | Local mode when not using plaintext credentials |
| Session JWT | Env, `*_FILE`, or `/run/ati/…` token files | `ati token issue`, Python `issue_token` | Proxy client (`Authorization: Bearer`), local scope enforcement |

<Note>
`ati init --proxy` writes JWT material into `config.toml` and PEM files under `~/.ati/`, but **`ati proxy` loads JWT keys only from `ATI_JWT_*` environment variables** (`jwt::config_from_env`). Map values from `config.toml` into the process environment (systemd, container spec, or shell exports) before starting the proxy.
</Note>

```text
  Orchestrator                    Proxy host                         Sandbox
  -------------                   ----------                         -------
  ati token issue  ──or──         ATI_JWT_PUBLIC_KEY                 ATI_PROXY_URL
  issue_token()       JWT mint    ATI_JWT_SECRET (+ optional         ATI_SESSION_TOKEN
                      │           ATI_JWT_ACCEPTED_AUDIENCES)              │
                      │                    │                               │
                      └──── inject ────────┼──── validate Bearer ──────────┘
                                           │
  ati key set ──► ~/.ati/credentials ──────┴──► keyring ──► upstream APIs
```

## Manage upstream API keys (`ati key`)

`ati key` reads and writes a plaintext JSON map at `~/.ati/credentials` (override base with `ATI_DIR`). Each `set` overwrites the file with pretty-printed JSON and sets Unix permissions to `0600`.

<Steps>
<Step title="Store a provider key">

```bash
ati key set finnhub_api_key "<secret>"
ati key set github_token ghp_xxxxxxxx
```

Names must match `auth_key_name` (and related fields) in provider manifests.

</Step>
<Step title="List or remove">

```bash
ati key list
# finnhub_api_key          sk-1...cdef

ati key remove finnhub_api_key
```

`list` masks values: short secrets show `***`; longer values show first/last 2–4 characters with `...` in between.

</Step>
</Steps>

The proxy loads the same file (unless started with `--env-keys`, which scans `ATI_KEY_*` env vars instead). Values may use `@file:/path/to/secret` when loaded through `Keyring::load_credentials` for mounted secrets.

<Warning>
Plaintext `credentials` is the **dev / proxy-host** path documented in AGENTS.md. Local encrypted mode uses `keyring.enc` and a one-shot session key file — a different storage path with the same logical key names.
</Warning>

## JWT signing and verification (`ati token`)

### Generate keys (`ati token keygen`)

<Tabs>
<Tab title="ES256 (recommended)">

```bash
ati token keygen --algorithm ES256
```

Prints PKCS#8 **private** PEM (issuance only) and **public** PEM (validation / JWKS). Save to files and set:

```bash
export ATI_JWT_PRIVATE_KEY=~/.ati/jwt-private.pem
export ATI_JWT_PUBLIC_KEY=~/.ati/jwt-public.pem
```

</Tab>
<Tab title="HS256 (single-machine)">

```bash
ati token keygen --algorithm HS256
```

Prints a 64-character hex secret. Set:

```bash
export ATI_JWT_SECRET=<hex output>
```

The proxy hex-decodes `ATI_JWT_SECRET` before use. The Python client expects the same hex format.

</Tab>
</Tabs>

### Issue a scoped token (`ati token issue`)

<ParamField body="--sub" type="string" required>
Agent or sandbox identity (`sub` claim).
</ParamField>

<ParamField body="--scope" type="string" required>
Space-delimited scopes (RFC 9068-style `scope` claim), e.g. `tool:finnhub:* skill:research-* help`.
</ParamField>

<ParamField body="--ttl" type="number" default="1800">
Lifetime in seconds (default 30 minutes).
</ParamField>

<ParamField body="--aud" type="string" default="ati-proxy">
Audience claim; must match an entry in the proxy’s accepted-audience allowlist.
</ParamField>

<ParamField body="--iss" type="string">
Issuer; defaults to `ATI_JWT_ISSUER` when omitted.
</ParamField>

<ParamField body="--key" type="path">
ES256 private key PEM for signing (else `ATI_JWT_PRIVATE_KEY` / `ATI_JWT_SECRET`).
</ParamField>

<ParamField body="--secret" type="string">
HS256 hex secret for signing.
</ParamField>

<ParamField body="--rate" type="string" repeatable>
Per-pattern rate limits in the `ati` namespace, e.g. `tool:github:*=10/hour`.
</ParamField>

```bash
ati token issue \
  --sub sandbox:job-42 \
  --scope "tool:finnhub:* tool:github:* help" \
  --ttl 3600 \
  --secret <hex-from-init-or-keygen>

export ATI_SESSION_TOKEN="<printed token>"
```

Issued tokens include optional `jti` (UUID), `ati: { v: 1, rate: {...}, customer_id?: ... }`, and optional `job_id` / `sandbox_id` when set by an orchestrator.

### Inspect and validate

```bash
ati token inspect "$ATI_SESSION_TOKEN"
ati token validate "$ATI_SESSION_TOKEN" --secret <hex>
# or: --key ~/.ati/jwt-public.pem
```

- **inspect** — decodes claims without signature verification (debugging only).
- **validate** — full verify (signature, `exp`, `aud` against `parse_audiences_env()`, optional `iss`); prints `VALID` to logs and **exits 1** on failure.

Validation audience resolution mirrors the proxy: `ATI_JWT_ACCEPTED_AUDIENCES` (CSV) → `ATI_JWT_AUDIENCE` → `["ati-proxy"]`. An empty allowlist is rejected (defense against accepting any `aud`).

### Check the active session (`ati auth status`)

```bash
ati auth status
```

Resolves `ATI_SESSION_TOKEN` (including file fallbacks), inspects claims, and reports tool/skill/help scope counts, expiry, and whether a local verification key is available.

## Initialize proxy-oriented layout (`ati init --proxy`)

```bash
ati init --proxy              # HS256 secret embedded in config.toml
ati init --proxy --es256      # jwt-private.pem + jwt-public.pem (private key 0600)
```

Creates `manifests/`, `specs/`, `skills/`, and **overwrites** `config.toml` with a `[proxy]` + `[proxy.jwt]` section:

| Mode | Generated artifacts | `config.toml` hints |
|------|---------------------|---------------------|
| HS256 (default) | Random 32-byte hex `secret` | `algorithm = "HS256"`, inline `secret` |
| ES256 (`--es256`) | `jwt-private.pem`, `jwt-public.pem` | `algorithm = "ES256"`, `private_key` / `public_key` paths |

Without `--proxy`, `init` only creates the directory skeleton and a minimal `config.toml` if missing (idempotent; does not overwrite existing config).

After init, export JWT env vars for the proxy process, then issue tokens:

```bash
# Example: HS256 from init output
export ATI_JWT_SECRET=<secret from config.toml>
export ATI_JWT_AUDIENCE=ati-proxy

ati proxy --port 8090 --ati-dir ~/.ati
ati token issue --sub agent-1 --scope "tool:* help" --secret "$ATI_JWT_SECRET"
```

## Proxy JWT environment contract

| Variable | Role |
|----------|------|
| `ATI_JWT_PUBLIC_KEY` | Path to ES256 public PEM → enables validation + `/.well-known/jwks.json` |
| `ATI_JWT_PRIVATE_KEY` | Path to ES256 private PEM → enables `ati token issue` on the host |
| `ATI_JWT_SECRET` | Hex HS256 secret → sign and verify (symmetric) |
| `ATI_JWT_ISSUER` | If set, validated tokens must match this `iss` |
| `ATI_JWT_ACCEPTED_AUDIENCES` | CSV allowlist; token `aud` must match **any** entry |
| `ATI_JWT_AUDIENCE` | Singular fallback when CSV unset (default `ati-proxy`) |

When `jwt_config` is present, all proxy routes except `/health` and `/.well-known/jwks.json` require `Authorization: Bearer <jwt>`. Without JWT config, the proxy runs in **dev mode** (unauthenticated), matching local CLI behavior when no `ATI_JWT_*` vars are set.

Clock skew tolerance is **60 seconds** (`leeway_secs`).

## Session token resolution and hot rotation

`core::token::resolve_token` (and `resolve_session_token`) pick the bearer sent to the proxy:

1. Non-empty `<NAME>` environment variable  
2. `<NAME>_FILE` path (if set)  
3. Default file from `default_token_file(<NAME>)`

| Env var | Default file when unset |
|---------|------------------------|
| `ATI_SESSION_TOKEN` | `/run/ati/session_token` (hardcoded; not `/run/ati/ati`) |
| `PARCHA_TOOLS_SESSION_TOKEN` | `/run/ati/parcha_tools` |
| Other `*_SESSION_TOKEN` | `/run/ati/<slug>` (suffix stripped, lowercased) |

Each CLI invocation **re-reads** the file (no in-process cache) so an external supervisor can rotate tokens atomically for long-lived agent processes.

<Info>
Set `ATI_SESSION_TOKEN_FILE` (or `<NAME>_FILE`) when the token should not live in the environment. Empty env values fall through to the file path.
</Info>

## Per-provider session token env overrides

Manifest field `auth_session_token_env` names which env var supplies the bearer for tools on that provider (issue #121 — audience separation):

```toml
[provider]
name = "parcha_tools"
# ...
auth_session_token_env = "PARCHA_TOOLS_SESSION_TOKEN"
```

Flow:

1. Orchestrator mints a JWT with `aud` set to a dedicated audience (e.g. `parcha-custom-tools`).
2. Sandbox receives `PARCHA_TOOLS_SESSION_TOKEN` (and optionally `ATI_SESSION_TOKEN` for other tools).
3. `ati run parcha_tools:some_tool` resolves the provider’s env var via `proxy/client.rs` → `resolve_token`.
4. Proxy must list that audience in `ATI_JWT_ACCEPTED_AUDIENCES`.

If the per-provider env is unset, empty, or unreadable, the client **falls back to `ATI_SESSION_TOKEN`** (still sends Authorization when possible). The proxy then accepts or rejects based on the fallback token’s `aud`.

```mermaid
sequenceDiagram
    participant Orch as Orchestrator
    participant SB as Sandbox
    participant CLI as ati run
    participant PX as ati proxy

    Orch->>Orch: issue_token(aud=parcha-custom-tools)
    Orch->>SB: PARCHA_TOOLS_SESSION_TOKEN
    Orch->>SB: ATI_SESSION_TOKEN (default aud)
    SB->>CLI: ati run parcha_tools:tool
    CLI->>CLI: resolve_token(PARCHA_TOOLS_SESSION_TOKEN)
    CLI->>PX: POST /call Authorization Bearer
    PX->>PX: validate aud in accepted_audiences
    PX-->>CLI: result
```

Example multi-audience proxy configuration:

```bash
export ATI_JWT_ACCEPTED_AUDIENCES="ati-proxy,parcha-custom-tools"
```

## Orchestrator token issuance (Python `ati-client`)

`AtiOrchestrator` in `ati-client/python` mints HS256 tokens compatible with the Rust proxy:

```python
from ati import AtiOrchestrator, issue_token, validate_token, build_scope_string

orch = AtiOrchestrator(
    proxy_url="http://proxy:8090",
    secret="<64-char-hex ATI_JWT_SECRET>",
    default_aud="ati-proxy",
    default_iss="ati-orchestrator",
)

env = orch.provision_sandbox(
    agent_id="sandbox:abc123",
    tools=["finnhub_quote", "web_search"],
    skills=["financial-analysis"],
    extra_scopes=["help"],
    ttl_seconds=3600,
    rate={"tool:github:*": "10/hour"},
    customer_id="cust_alpha",  # optional; proxy credential cascade
)
# env["ATI_PROXY_URL"], env["ATI_SESSION_TOKEN"]
# optional env["skills"] when fetch_skill_content=True
```

`build_scope_string` prefixes `tool:` and `skill:` automatically:

```python
build_scope_string(tools=["web_search", "github:*"], skills=["research-*"])
# → "tool:web_search tool:github:* skill:research-*"
```

Lower-level issuance without the orchestrator wrapper:

```python
token = issue_token(
    secret=hex_secret,
    sub="agent-7",
    scope="tool:finnhub:* help",
    ttl_seconds=1800,
    aud="ati-proxy",
    customer_id=None,
)
validate_token(token, secret=hex_secret, audience="ati-proxy", issuer="ati-orchestrator")
```

Inject returned env vars into the sandbox (Kubernetes, VM, or agent harness). See [Agent harness sandbox](/agent-harness-sandbox) for shell-first integration patterns.

## JWT claims reference (ATI profile)

| Claim | Required | Purpose |
|-------|----------|---------|
| `sub` | yes | Agent / sandbox identity |
| `aud` | yes | Target service; matched against proxy allowlist |
| `iat`, `exp` | yes | Issued-at and expiry (Unix seconds) |
| `scope` | yes | Space-delimited authorization (tools, skills, `help`, `*`) |
| `iss` | no | Issuer when `ATI_JWT_ISSUER` enforced |
| `jti` | no | Unique ID (issued by default from CLI) |
| `ati` | no | Namespace: `v`, `rate`, `customer_id` |
| `job_id`, `sandbox_id` | no | Orchestrator metadata |

Scopes are enforced **after** JWT validation via `ScopeConfig` (wildcards such as `tool:github:*`). Detailed grammar is on [JWT and scopes reference](/jwt-scopes-reference).

## Operational checklist

<Steps>
<Step title="Prepare ~/.ati">

```bash
ati init --proxy --es256   # or --proxy for HS256
ati key set <auth_key_name> <value>
```

</Step>
<Step title="Configure proxy JWT env">

Map `config.toml` / PEM paths to `ATI_JWT_PUBLIC_KEY`, `ATI_JWT_PRIVATE_KEY` or `ATI_JWT_SECRET`, plus `ATI_JWT_ACCEPTED_AUDIENCES` when using per-provider audiences.

</Step>
<Step title="Start proxy and mint sandbox token">

```bash
ati proxy --port 8090 --ati-dir ~/.ati
ati token issue --sub <id> --scope "<scopes>" --key ~/.ati/jwt-private.pem
```

</Step>
<Step title="Inject sandbox env">

```bash
export ATI_PROXY_URL=http://127.0.0.1:8090
export ATI_SESSION_TOKEN=<jwt>
# optional: echo <jwt> | sudo tee /run/ati/session_token
```

</Step>
<Step title="Verify">

```bash
ati auth status
ati token validate "$ATI_SESSION_TOKEN"
curl -s http://127.0.0.1:8090/health
curl -s http://127.0.0.1:8090/.well-known/jwks.json   # when ES256 + public key configured
```

</Step>
</Steps>

### Troubleshooting

| Symptom | Likely cause |
|---------|----------------|
| Proxy logs `DISABLED (no JWT keys configured)` | No `ATI_JWT_PUBLIC_KEY` / `ATI_JWT_SECRET` in proxy environment |
| `401` with valid-looking token | Wrong `aud` vs `ATI_JWT_ACCEPTED_AUDIENCES`; expired `exp`; wrong signing secret |
| `ati token validate` exits 1 | Mismatched `--key`/`--secret` or audience/issuer env vs token claims |
| Local `ati run` denies tools with JWT configured | Missing/invalid `ATI_SESSION_TOKEN` while `ATI_JWT_*` set locally (`load_local_scopes_from_env`) |
| Per-provider token ignored | `auth_session_token_env` unset in manifest; env empty and file missing |
| `ATI_JWT_SECRET is not valid hex` | Secret not hex-encoded (use `ati token keygen HS256` or `ati init --proxy`) |

## Related pages

<CardGroup>
<Card title="Execution modes" href="/execution-modes">
Dev credentials file vs encrypted keyring vs proxy mode — where each key type lives.
</Card>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
Run `ati proxy`, JWKS health probes, `--env-keys`, and production systemd examples.
</Card>
<Card title="JWT and scopes reference" href="/jwt-scopes-reference">
Scope grammar, claim validation failures, and wildcard rules.
</Card>
<Card title="Environment variables" href="/environment-variables">
Full `ATI_JWT_*`, `ATI_SESSION_TOKEN*`, and `ATI_KEY_*` contract.
</Card>
<Card title="Agent harness sandbox" href="/agent-harness-sandbox">
`provision_sandbox` env injection and proxy-mode agent loops.
</Card>
<Card title="Security and production" href="/security-and-production">
Threat model, proxy hardening, and optional virtual keys / audit DB.
</Card>
</CardGroup>

---

## 13. File manager operations

> `file_manager:download` and `file_manager:upload`, SSRF protection, download allowlists, upload destination kinds (`gcs`, `fal_storage`), and proxy-side base64 transfer.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/13-file-manager-operations.md
- Generated: 2026-06-02T03:17:10.249Z

### Source Files

- `src/core/file_manager.rs`
- `src/cli/file_manager.rs`
- `src/core/gcs.rs`
- `tests/file_manager_proxy_test.rs`
- `README.md`
- `src/core/manifest.rs`

---
title: "File manager operations"
description: "`file_manager:download` and `file_manager:upload`, SSRF protection, download allowlists, upload destination kinds (`gcs`, `fal_storage`), and proxy-side base64 transfer."
---

The built-in `file_manager` handler exposes two virtual tools—`file_manager:download` and `file_manager:upload`—registered automatically on every `ManifestRegistry` load with no operator manifest required for downloads. In proxy mode the proxy performs outbound HTTP and storage I/O while binary payloads move over `POST /call` as base64 JSON; the sandbox-side CLI reads local files for upload, writes `--out` paths for download, and strips CLI-only arguments before forwarding work upstream.

## When to use file manager

| Tool | Scope claim | Use when |
|------|-------------|----------|
| `file_manager:download` | `tool:file_manager:download` | An agent has a URL and needs bytes (media, PDFs, CSVs, archives) written locally or returned inline. |
| `file_manager:upload` | `tool:file_manager:upload` | An agent has a local file and needs a public URL from an operator-allowlisted sink (`gcs`, `fal_storage`). |

<Note>
Without `~/.ati/manifests/file_manager.toml` declaring upload destinations, `file_manager:upload` returns `UploadNotConfigured` (HTTP 503 on the proxy). Downloads still work; only uploads are gated.
</Note>

## Execution split: proxy vs local

```mermaid
sequenceDiagram
    participant Agent as Sandbox agent
    participant CLI as cli/file_manager.rs
    participant Proxy as proxy/server.rs
    participant Core as core/file_manager.rs
    participant Net as External URL or storage

    Agent->>CLI: ati run file_manager:download --url ... --out /tmp/x
    alt ATI_PROXY_URL set
        CLI->>Proxy: POST /call (url, max_bytes, headers, ...)
        Proxy->>Core: dispatch_file_manager → fetch_bytes
        Core->>Net: GET url (SSRF + allowlist)
        Net-->>Core: response body
        Core-->>Proxy: JSON + content_base64
        Proxy-->>CLI: result JSON
        CLI->>CLI: decode base64, write --out
    else Local mode
        CLI->>Core: fetch_bytes inline
        Core->>Net: GET url
        Net-->>Core: bytes
        CLI->>CLI: decode/write or inline base64
    end
    CLI-->>Agent: formatted output
```

Upload reverses the byte direction: the CLI reads `--path`, base64-encodes (dropping the raw `Vec` before the HTTP call to limit peak RAM), sends `content_base64` + `filename` to local core or `POST /call`, and the proxy uploads to the resolved destination using keyring credentials.

## `file_manager:download`

### CLI arguments

<ParamField body="url" type="string" required>
HTTPS/HTTP URL to fetch. Required on the wire; stripped from proxy args when using `--out` only on the CLI side.
</ParamField>

<ParamField body="out" type="string">
Local path to write decoded bytes. CLI-only: removed before the proxy/local fetch. When omitted, the CLI keeps `content_base64` in the formatted response.
</ParamField>

<ParamField body="inline" type="boolean">
Documented no-op when `--out` is absent—base64 is always returned inline without `--out`.
</ParamField>

<ParamField body="max_bytes" type="integer">
Abort if the body exceeds this size. Default **500 MB** (`DEFAULT_MAX_BYTES`). Also accepts `max-bytes` alias.
</ParamField>

<ParamField body="timeout" type="integer">
Upstream fetch timeout in seconds. Default **120** (`DEFAULT_TIMEOUT_SECS`).
</ParamField>

<ParamField body="follow_redirects" type="boolean">
Follow up to 10 redirects when true (default). Set false to use `Policy::none()`.
</ParamField>

<ParamField body="headers" type="object | JSON string">
Extra request headers as a JSON object or JSON-encoded string. Values must be string, number, or bool.
</ParamField>

### Denied download headers

Agents cannot set: `host`, `content-length`, `transfer-encoding`, `connection`, `proxy-authorization`. Invalid header names (non-ASCII or containing `:`) are rejected with HTTP 400.

### Successful response (wire / proxy `result`)

<ResponseField name="success" type="boolean">
Always `true` on success.
</ResponseField>

<ResponseField name="size_bytes" type="integer">
Length of decoded body.
</ResponseField>

<ResponseField name="content_type" type="string | null">
Upstream `Content-Type` when present.
</ResponseField>

<ResponseField name="source_url" type="string">
Original request URL.
</ResponseField>

<ResponseField name="content_base64" type="string">
Standard base64 of raw bytes. Always present on the proxy/local core response; the CLI omits it from the final output when `--out` is set and adds `path` instead.
</ResponseField>

<RequestExample>

```bash
ati run file_manager:download \
  --url "https://v3b.fal.media/files/b/.../output.mp4" \
  --out /tmp/clip.mp4
```

</RequestExample>

<ResponseExample>

```json
{
  "success": true,
  "size_bytes": 8655798,
  "content_type": "video/mp4",
  "source_url": "https://v3b.fal.media/files/b/.../output.mp4",
  "path": "/tmp/clip.mp4"
}
```

</ResponseExample>

### Download security layers

Downloads apply **two independent checks** before the HTTP client runs:

1. **SSRF protection** — `validate_url_not_private` from `core/http.rs`, gated by `ATI_SSRF_PROTECTION`:
   - `1` or `true`: block loopback, RFC1918, link-local, `.internal`/`.local` hostnames, and resolved private IPs.
   - `warn`: log and allow.
   - unset/other: allow (intended for local dev).

2. **Host allowlist** — `ATI_DOWNLOAD_ALLOWLIST` (comma-separated, case-insensitive):
   - unset/empty: any non-private host allowed.
   - set: host must match an exact pattern, `*.suffix` subdomain wildcard, or bare `*`.
   - Suffix collisions like `evilfal.media` against `*.fal.media` are rejected.

<Warning>
Production proxies should set **both** `ATI_SSRF_PROTECTION=1` and `ATI_DOWNLOAD_ALLOWLIST`. An unset allowlist lets scoped agents fetch from arbitrary public hosts through the proxy.
</Warning>

The fetch streams the body and enforces `max_bytes` during read; `Content-Length` is checked up front when present. Non-success upstream statuses surface as errors with the upstream status clamped into HTTP 4xx–5xx on `POST /call`.

## `file_manager:upload`

### CLI arguments

<ParamField body="path" type="string" required>
Local file to read. CLI-only; never sent on the wire.
</ParamField>

<ParamField body="destination" type="string">
Key from `[provider.upload_destinations.<name>]` in `file_manager.toml`. Omit to use `upload_default_destination`.
</ParamField>

<ParamField body="content_type" type="string">
Override MIME type; default from extension via `guess_content_type`.
</ParamField>

<ParamField body="object_name" type="string">
Override object key / filename sent on the wire (sanitized: directory components and `..` stripped).
</ParamField>

### Wire arguments (proxy / local core)

<ParamField body="filename" type="string" required>
Sanitized basename used as GCS object suffix or `X-Fal-File-Name`.
</ParamField>

<ParamField body="content_base64" type="string" required>
Base64 payload. Hard cap **1 GB** decoded (`MAX_UPLOAD_BYTES`).
</ParamField>

<ParamField body="content_type" type="string">
MIME type for upload.
</ParamField>

<ParamField body="destination" type="string">
Allowlist key; optional if operator set `upload_default_destination`.
</ParamField>

### Successful response

<ResponseField name="success" type="boolean">
`true` on success.
</ResponseField>

<ResponseField name="url" type="string">
Public-style URL from the sink (GCS path URL or fal `access_url`).
</ResponseField>

<ResponseField name="size_bytes" type="integer">
Uploaded byte count.
</ResponseField>

<ResponseField name="content_type" type="string">
MIME used for upload.
</ResponseField>

<ResponseField name="destination" type="string">
Resolved allowlist key (e.g. `fal`, `gcs`).
</ResponseField>

<RequestExample>

```bash
ati key set fal_api_key "your-fal-key"
ati run file_manager:upload --path /tmp/example.png --destination fal
```

</RequestExample>

<ResponseExample>

```json
{
  "success": true,
  "url": "https://v3b.fal.media/files/b/.../example.png",
  "size_bytes": 15236,
  "content_type": "image/png",
  "destination": "fal"
}
```

</ResponseExample>

## Upload destination kinds

Operators declare sinks under `[provider.upload_destinations.<key>]` with a `kind` tag. Agents may only use keys present in that map.

| `kind` | TOML fields | Behavior |
|--------|-------------|----------|
| `gcs` | `bucket`, `prefix` (default `ati-uploads`), `key_ref` (default `gcp_credentials`) | Service-account JSON from keyring → `GcsClient::new_read_write` → object at `<prefix>/<YYYY-MM-DD>/<uuid>-<filename>` via GCS JSON simple upload. Returns `https://storage.googleapis.com/<bucket>/<encoded-path>`. |
| `fal_storage` | `key_ref` (default `fal_api_key`), `endpoint` (optional, default `https://rest.alpha.fal.ai`) | POST `{endpoint}/storage/auth/token?storage_type=fal-cdn-v3` with `Authorization: Key <api_key>`, then POST `{base_url}/files/upload` with signed token headers and `X-Fal-File-Name`. Returns fal `access_url`. |

### Server-supplied URL SSRF (fal upload)

URLs returned by fal's token response (`base_url` + `/files/upload`) pass through `require_public_https_url`, which **always** enforces HTTPS and private-IP rejection—**independent** of `ATI_SSRF_PROTECTION`. This blocks a compromised fal endpoint from redirecting uploads to metadata or RFC1918 targets.

## Operator manifest

Drop `~/.ati/manifests/file_manager.toml` to allow uploads. If the manifest declares `handler = "file_manager"` but no `[[tools]]`, built-in download/upload tools are attached automatically.

```toml
[provider]
name = "file_manager"
description = "Generic binary download/upload"
handler = "file_manager"
upload_default_destination = "fal"

[provider.upload_destinations.fal]
kind = "fal_storage"
key_ref = "fal_api_key"

[provider.upload_destinations.gcs]
kind = "gcs"
bucket = "my-uploads"
prefix = "ati-uploads"
key_ref = "gcp_credentials"
```

Load-time validation refuses `upload_default_destination` values not present in `upload_destinations`.

<Tip>
Set keyring credentials before upload: `ati key set fal_api_key "..."` and `ati key set gcp_credentials "@file:/path/to/sa.json"`.
</Tip>

## Proxy `POST /call` behavior

- Handler dispatch: `provider.handler == "file_manager"` → `dispatch_file_manager` in `proxy/server.rs`.
- Download errors map through `FileManagerError::http_status` (400 validation, 403 SSRF/allowlist/unknown destination, 413 size cap, 502/503 upstream, etc.).
- Body limit: `DefaultBodyLimit` and `max_call_body_bytes()` are sized for ~1 GB raw upload base64 (~4/3 inflation + framing), so large uploads are not rejected at axum's default 2 MB ceiling.

## Size and memory limits

| Limit | Value | Applies to |
|-------|-------|------------|
| `DEFAULT_MAX_BYTES` | 500 MB | Download default `max_bytes` |
| `MAX_UPLOAD_BYTES` | 1 GB | Decoded upload payload |
| `DEFAULT_TIMEOUT_SECS` | 120 s | Download HTTP timeout |
| Proxy `/call` body | ~1.34× `MAX_UPLOAD_BYTES` + 8 KB | Outer wire cap before per-tool checks |

The upload CLI encodes to base64 then `drop(bytes)` before calling the proxy so peak RAM stays near one copy of the payload plus base64, not both simultaneously.

## Common errors

| Error / symptom | Typical HTTP status | Cause |
|-----------------|---------------------|-------|
| `Host '…' is not in the download allowlist` | 403 | `ATI_DOWNLOAD_ALLOWLIST` set; host mismatch |
| `URL is not allowed (private/internal address)` | 403 | `ATI_SSRF_PROTECTION` blocking private target |
| `Response exceeds max-bytes` | 413 | Download body or upload over cap |
| `Upload destinations not configured` | 503 | Empty `upload_destinations` |
| `Unknown upload destination '…'` | 403 | `--destination` not in manifest map |
| `keyring key '…' missing` | 502 (upload wrapper) | Missing `fal_api_key` or `gcp_credentials` |

## Related pages

<CardGroup>
  <Card title="Providers and handlers" href="/providers-and-handlers">
    How `file_manager` fits alongside `http`, `mcp`, `openapi`, `cli`, and `passthrough` handlers.
  </Card>
  <Card title="Execution modes" href="/execution-modes">
    Local vs proxy credential placement and `ATI_PROXY_URL` auto-detection.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    `ATI_SSRF_PROTECTION`, `ATI_DOWNLOAD_ALLOWLIST`, and proxy session variables.
  </Card>
  <Card title="Security and production" href="/security-and-production">
    Hardening proxies with download allowlists, SSRF, and JWT scope enforcement.
  </Card>
  <Card title="Proxy API reference" href="/proxy-api-reference">
    `POST /call` request/response shape for tool execution.
  </Card>
  <Card title="Manifest reference" href="/manifest-reference">
    Full `[provider]` schema including `upload_destinations` and `upload_default_destination`.
  </Card>
</CardGroup>

---

## 14. Skills registry and fetch

> `ati skill install|resolve`, manifest generation from SKILL.md, GCS bucket layout, `ati skill fetch` / `skillati` commands, and proxy `/skillati/*` endpoints.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/14-skills-registry-and-fetch.md
- Generated: 2026-06-02T03:17:19.101Z

### Source Files

- `src/cli/skills.rs`
- `src/core/skillati.rs`
- `src/core/skill.rs`
- `src/proxy/server.rs`
- `scripts/test_skills_e2e.sh`
- `scripts/test_skill_fetch_e2e.sh`

---
title: "Skills registry and fetch"
description: "`ati skill install|resolve`, manifest generation from SKILL.md, GCS bucket layout, `ati skill fetch` / `skillati` commands, and proxy `/skillati/*` endpoints."
---

ATI maintains two skill surfaces: **local** skills under `~/.ati/skills/` (installed, resolved, and served by `/skills*`) and **remote** skills in a GCS bucket or upstream proxy (listed and fetched lazily via `ati skill fetch` / `ati skillati` and `/skillati/*`). Install and resolve operate on disk; fetch and SkillATI endpoints implement progressive disclosure (catalog metadata → SKILL.md activation → on-demand resources) without copying remote content into `~/.ati/skills/` unless you explicitly install.

## Local registry layout

Each installed skill is a directory:

```text
~/.ati/skills/<skill-name>/
├── SKILL.md          # Methodology body (+ optional YAML frontmatter)
├── skill.toml        # Optional ATI bindings (tools, providers, categories, depends_on)
├── references/       # Optional on-demand docs
├── scripts/          # Optional helpers
├── assets/           # Optional templates/data
└── provider.toml     # Optional bundled provider manifest (install fallback)
```

`SkillRegistry::load` indexes skills by name, tool, provider, and category. Metadata priority is YAML frontmatter in `SKILL.md`, then `skill.toml`, then inferred fields.

## Install skills

`ati skill install` copies skill trees into `~/.ati/skills/`. Writes always happen locally; install does not route through the proxy when `ATI_PROXY_URL` is set.

<ParamField body="source" type="string" required>
Path, HTTPS/git URL, or URL with `#subdir` fragment. Git sources may pin with `@<sha>` when the suffix is 7+ hex digits.
</ParamField>

<ParamField body="--from-git" type="string">
Explicit git URL (deprecated; URLs are auto-detected).
</ParamField>

<ParamField body="--name" type="string">
Override destination directory name.
</ParamField>

<ParamField body="--all" type="flag">
Install every child directory that contains `SKILL.md` or `skill.toml`.
</ParamField>

<ParamField body="--local" type="flag">
Use Ollama only for optional manifest generation (no network LLM fallback).
</ParamField>

<Steps>
<Step title="Install from a local directory">

```bash
ati skill install ./my-skill/
```

Copies into `~/.ati/skills/my-skill/`, records `[ati.integrity]` in `skill.toml` when `SKILL.md` exists, and may generate a provider manifest (see below).

</Step>

<Step title="Install from git">

```bash
ati skill install https://github.com/org/repo#skill-name
ati skill install https://github.com/org/repo@abc1234#skill-name
```

Shallow clone by default; full clone when a SHA pin is present, then checkout.

</Step>

<Step title="Verify installation">

```bash
ati skill list
ati skill info my-skill
ati skill validate my-skill --check-tools
```

</Step>
</Steps>

### Manifest generation from SKILL.md

After copy, `generate_manifest_from_skill` may create `~/.ati/manifests/<provider>.toml` from `SKILL.md` using an LLM:

| Condition | Backend |
|-----------|---------|
| `--local` or `ATI_MANIFEST_PROVIDER=local` | Ollama only (errors if unavailable; no network fallback) |
| `ATI_MANIFEST_PROVIDER=cerebras` | `CEREBRAS_API_KEY` |
| `ATI_MANIFEST_PROVIDER=anthropic` | `ANTHROPIC_API_KEY` |
| Default | Cerebras, then Anthropic |
| LLM failure or invalid TOML | `provider.toml` bundled in the skill directory |

Generation is skipped when the target manifest already exists or `SKILL.md` is missing. Provider name comes from `skill.toml` `providers = [...]` or the skill directory name.

<Note>
Installing a skill never edits existing provider manifests in place; a collision prints `Provider '<name>' already has a manifest, skipping generation.` and leaves the file unchanged.
</Note>

## Resolve skills for scopes

`ati skill resolve` answers which **local** skills auto-load for JWT scopes. It loads `~/.ati/skills/`, manifests under `~/.ati/manifests/`, and scopes from the environment or `--scopes <path>` (JSON with `scopes` array and optional `agent_id`).

Resolution cascade (local `skill::resolve_skills`):

1. `skill:X` → skill X
2. `tool:Y` → skills whose `tools` include Y
3. Tool Y’s provider → skills bound to that provider
4. Provider category → skills bound to that category
5. `depends_on` → transitive dependencies
6. Legacy underscore tool scopes via `filter_tools_by_scope`

Wildcard scope `*` lists the registry but does **not** auto-load every skill.

<ResponseExample>

```json
[
  {
    "name": "compliance-screening",
    "version": "1.0.0",
    "description": "Screen entities against sanctions lists",
    "tools": ["ca_person_sanctions_search"],
    "providers": ["complyadvantage"],
    "categories": ["compliance"]
  }
]
```

</ResponseExample>

<Warning>
`POST /skills/resolve` on the proxy resolves **local** `SkillRegistry` entries only, filtered by the caller’s visible local skill names. Remote GCS skills are not included in that response; use `GET /skillati/catalog` plus per-skill `GET /skillati/:name` for remote methodology.
</Warning>

When `ATI_PROXY_URL` is set, read-only skill commands (`list`, `show`, `search`, `info`, `read`, `resolve`) forward to `/skills` on the proxy; `install`, `remove`, `init`, and `validate` still run locally.

## Remote GCS bucket layout

Remote skills use one GCS prefix per skill name. Object keys are `{skill-name}/{relative-path}` (forward slashes). `SKILL.md` is required for discovery; `skill.toml` is optional metadata at install/build time.

```text
gs://<bucket>/
├── _skillati/catalog.v1.json     # Recommended catalog index (fast path)
├── compliance-screening/
│   ├── SKILL.md
│   └── skill.toml
├── fal-generate/
│   ├── SKILL.md
│   ├── scripts/generate.sh
│   └── references/usage.md
└── ...
```

### Catalog index manifest

Build the index offline from a local skills tree:

```bash
ati skill fetch build-index ./skills/ --output-file catalog.json
# Upload to gs://<bucket>/_skillati/catalog.v1.json
```

`build_catalog_manifest` walks either a single skill directory (if it contains `SKILL.md`) or immediate child directories with `SKILL.md`. Each entry includes `RemoteSkillMeta` (name, description, `when_to_use`, bindings, keywords) plus a `resources` list (visible files only; excludes `SKILL.md`, `skill.toml`, dotfiles).

| Field | Meaning |
|-------|---------|
| `version` | Catalog schema version (default `1`) |
| `generated_at` | RFC3339 timestamp |
| `skills[]` | Entries with `meta`, `resources`, `resources_complete` |

Index lookup order (override with comma-separated `ATI_SKILL_REGISTRY_INDEX_OBJECT`):

1. `_skillati/catalog.v1.json` (default)
2. `_skillati/catalog.json`
3. `skillati-catalog.json`

If no index loads, the client lists bucket skill prefixes and reads each `SKILL.md` concurrently (`FALLBACK_CATALOG_CONCURRENCY = 24`).

## Registry configuration

| Variable | Values | Role |
|----------|--------|------|
| `ATI_SKILL_REGISTRY` | `gcs://<bucket>` | Direct GCS via keyring `gcp_credentials` |
| `ATI_SKILL_REGISTRY` | `proxy` | Delegate to `ATI_PROXY_URL` `/skillati/*` (requires session token when enforced) |
| `ATI_SKILL_REGISTRY_INDEX_OBJECT` | Comma-separated object paths | Override catalog index candidates |
| `ATI_SKILL_FETCH_DISABLED` | `1` / `true` | Block fetch commands (except `build-index`); for pre-installed sandboxes |
| `ATI_PROXY_URL` | HTTP base | Routes `ati skill fetch` / `ati skillati` to proxy |
| `ATI_SESSION_TOKEN` | JWT | Bearer auth on proxy fetch |

Proxy startup with GCS:

```bash
ati key set gcp_credentials < /path/to/service-account.json
ATI_SKILL_REGISTRY=gcs://my-skills-bucket ati proxy --port 8090
```

## Progressive disclosure and fetch commands

`ati skill fetch <subcommand>` and `ati skillati <subcommand>` share the same implementation (`SkillAtiCommands`).

```mermaid
sequenceDiagram
  participant Agent
  participant CLI as ati skill fetch
  participant Proxy as ati proxy
  participant GCS as GCS bucket

  Agent->>CLI: catalog
  alt ATI_PROXY_URL set
    CLI->>Proxy: GET /skillati/catalog
    Proxy->>GCS: load index or list prefixes
  else gcs:// registry
    CLI->>GCS: read _skillati/catalog.v1.json
  end
  Agent->>CLI: read my-skill
  CLI->>Proxy: GET /skillati/my-skill
  Proxy->>GCS: GET my-skill/SKILL.md
  Proxy-->>Agent: SkillAtiActivation JSON
  Agent->>CLI: cat my-skill scripts/run.sh
  CLI->>Proxy: GET /skillati/my-skill/file?path=...
  Proxy->>GCS: GET my-skill/scripts/run.sh
```

| Level | Command | Payload |
|-------|---------|---------|
| 1 — Metadata | `catalog` [--search Q] | `RemoteSkillMeta` list (250-char listing budgets apply in orchestrators) |
| 2 — Instructions | `read <name>` | `SkillAtiActivation`: `name`, `description`, `skill_directory`, `content` (no `resources` field) |
| 3 — Resources | `resources`, `cat`, `refs`, `ref` | Paths under `references/`, `scripts/`, `assets/` |

Level-2 text output mirrors Claude Code’s activation shape:

```text
Base directory for this skill: skillati://<name>

<description>

<SKILL.md body>
```

Substitutions applied in `read_skill`:

- `${ATI_SKILL_DIR}` and `${CLAUDE_SKILL_DIR}` → `skillati://<name>`
- `.claude/skills/<other>/…` → `skillati://<other>/…` when `<other>` is a valid catalog name
- Bare `<catalog-skill>/(references|scripts|assets)/…` cross-links when the prefix matches a catalog entry

`cat` supports sibling skills via `../other-skill/path` (validated names only). Binary files require `--output json` (base64 in `SkillAtiFile`).

<ParamField body="ATI_SKILL_FETCH_DISABLED" type="env">
When set, fetch subcommands exit with guidance to use the host’s native Skill tool; `build-index` remains available for publishers.
</ParamField>

## Proxy `/skillati/*` endpoints

All handlers require `SkillAtiClient::from_env` (503 if `ATI_SKILL_REGISTRY` unset). JWT (or virtual key) scopes gate visibility via `visible_skill_names_with_remote` (local union + remote cascade mirroring `resolve_skills`). Denied skills return **404** (`SkillNotFound`), not 403.

| Method | Path | Query / body | Success body |
|--------|------|--------------|--------------|
| GET | `/skillati/catalog` | `?search=` (optional, max 25 matches) | `{ "skills": [ RemoteSkillMeta, ... ] }` |
| GET | `/skillati/{name}` | — | `SkillAtiActivation` JSON |
| GET | `/skillati/{name}/resources` | `?prefix=` | `{ name, prefix, resources: [] }` |
| GET | `/skillati/{name}/file` | `?path=` (required) | `SkillAtiFile` (`kind: text` or `binary`) |
| GET | `/skillati/{name}/refs` | — | `{ name, references: [] }` |
| GET | `/skillati/{name}/ref/{reference}` | — | `{ name, reference, content }` |

:::endpoint GET /skillati/catalog
List remote skills visible under the caller’s scopes. Optional fuzzy `search` filters name, description, keywords, tools, providers, and categories.
:::

:::endpoint GET /skillati/{name}
Return Level-2 activation: frontmatter stripped, directory variables and cross-skill paths rewritten to `skillati://` URIs. Does not embed the resource manifest.
:::

:::endpoint GET /skillati/{name}/file
Read any skill-relative path. `path=SKILL.md` is handled via the activation path on the proxy transport. Invalid or escaping paths → 400; missing object → 404.
:::

Error mapping (`skillati_error_response`):

| Condition | HTTP |
|-----------|------|
| Registry not configured / missing GCS creds | 503 |
| Skill or path not found | 404 |
| Invalid path | 400 |
| GCS or upstream proxy failure | 502 |

Remote visibility uses the same binding model as local resolve: explicit `skill:X`, then tool/provider/category matches from scoped tools. Wildcard scopes see the full remote catalog.

## Local vs remote API split

```text
                    ┌─────────────────────┐
                    │   Agent / CLI       │
                    └─────────┬───────────┘
                              │
          ┌───────────────────┼───────────────────┐
          ▼                   ▼                   ▼
   ~/.ati/skills/      ATI_SKILL_REGISTRY    JWT scopes
   (install/resolve)   gcs:// or proxy
          │                   │
          ▼                   ▼
   GET /skills*          GET /skillati/*
   POST /skills/resolve   (lazy GCS + cache)
```

| Concern | Local `/skills*` | Remote `/skillati/*` |
|---------|------------------|----------------------|
| Storage | `~/.ati/skills/` | GCS bucket (or proxy cache) |
| Install | `ati skill install` | Publish objects + catalog index |
| List | `ati skill list` | `ati skill fetch catalog` |
| Read body | `ati skill show/read` | `ati skill fetch read` |
| Scope resolve | `ati skill resolve`, `POST /skills/resolve` | Catalog + per-handler visibility |

## Verification

<Check>
`scripts/test_skills_e2e.sh` exercises install, resolve, bindings across HTTP/OpenAPI/MCP manifests, and proxy-mode list/read.
</Check>

<Check>
`scripts/test_skill_fetch_e2e.sh` boots a proxy against `gcs://parcha-ati-skills` (override with `ATI_E2E_BUCKET`) and asserts Level-2 shape: non-empty `description`, no `resources` field, `${ATI_SKILL_DIR}` substitution, and `.claude/skills/` rewrites.
</Check>

Integration tests in `tests/proxy_server_test.rs` cover scope-gated `/skillati/*` handlers, including remote-only skills with `ATI_SKILL_REGISTRY=proxy`.

Troubleshooting:

- **503 on `/skillati/*`**: set `ATI_SKILL_REGISTRY` and ensure `gcp_credentials` is in the proxy keyring for `gcs://` mode.
- **404 for a known remote skill**: token lacks `skill:<name>` or tool/provider/category cascade; wildcard scope is required for catalog-wide access.
- **Empty catalog**: upload `_skillati/catalog.v1.json` or ensure bucket prefixes contain `SKILL.md`.
- **Fetch blocked in sandbox**: `ATI_SKILL_FETCH_DISABLED` — use the host Skill tool or pre-install under `~/.ati/skills/`.

## Related pages

<CardGroup>
<Card title="Skills and SkillATI" href="/skills-and-skillati">
Progressive disclosure model, scope cascade overview, and how skills differ from tools.
</Card>
<Card title="Proxy API reference" href="/proxy-api-reference">
Full proxy route list including `/skills` bundle and auth requirements.
</Card>
<Card title="JWT and scopes reference" href="/jwt-scopes-reference">
`skill:`, `tool:`, and wildcard scope grammar used by SkillATI visibility.
</Card>
<Card title="Environment variables" href="/environment-variables">
`ATI_SKILL_REGISTRY`, index override, and fetch-disable flags.
</Card>
<Card title="CLI reference" href="/cli-reference">
Top-level `ati skill` and `ati skillati` command tree.
</Card>
</CardGroup>

---

## 15. CLI reference

> Top-level `ati` subcommands (`run`, `tool`, `provider`, `skill`, `assist`, `plan`, `key`, `token`, `auth`, `proxy`, `audit`, `edge`), global flags (`--output`, `--verbose`), and output formats.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/15-cli-reference.md
- Generated: 2026-06-02T03:17:23.520Z

### Source Files

- `src/main.rs`
- `src/cli/mod.rs`
- `src/cli/provider.rs`
- `src/output/mod.rs`
- `README.md`
- `src/cli/audit.rs`
- `src/cli/edge.rs`

---
title: "CLI reference"
description: "Top-level `ati` subcommands (`run`, `tool`, `provider`, `skill`, `assist`, `plan`, `key`, `token`, `auth`, `proxy`, `audit`, `edge`), global flags (`--output`, `--verbose`), and output formats."
---

The `ati` binary is a single Rust entrypoint (`src/main.rs`) built with **clap**. Every invocation runs `cli::common::ensure_ati_dir()` first, initializes structured logging (proxy vs CLI mode), dispatches to a handler under `src/cli/`, and on failure emits tracing errors plus optional structured JSON on stderr when `--output json` is active.

## Global flags

These flags apply to all subcommands.

| Flag | Env | Default | Effect |
|------|-----|---------|--------|
| `--output` / `--format` | `ATI_OUTPUT` | `json` | Selects `json`, `table`, or `text` formatting via `src/output/mod.rs` |
| `-J` / `--json` | — | off | Shorthand: forces JSON output (overrides `--output`) |
| `--verbose` | — | off | Enables debug tracing; with JSON errors, includes an error `chain` |

<ParamField body="--output" type="enum (json \| table \| text)" default="json">
Controls how successful command results are printed. Subcommands that emit fixed prose (for example `ati key list`) may ignore this for non-JSON paths.
</ParamField>

<ParamField body="-J, --json" type="boolean">
Global JSON shorthand. Also honored inside `ati run` when swallowed by `trailing_var_arg` (see `parse_tool_args` in `src/cli/call.rs`).
</ParamField>

<ParamField body="--verbose" type="boolean">
Sets tracing to debug and prints error cause chains on stderr for non-JSON failures.
</ParamField>

### Built-in help

Use `ati --help`, `ati -h`, or `ati <subcommand> -h` for clap-generated help. **`ati run` disables the global `--help` flag** so `ati run <tool> --help` can pass through to CLI-backed tools. For ATI’s own `run` help, use `ati run -h`.

## Output formats

Successful tool and discovery commands typically serialize a `serde_json::Value` and pass it through `output::format_output`:

| Format | Behavior |
|--------|----------|
| **json** | Pretty-printed JSON (`src/output/json.rs`) |
| **table** | `comfy-table` for arrays of objects or key–value objects; falls back to JSON for other shapes |
| **text** | Strings printed raw; objects as `key: value` lines; arrays one item per line |

<Note>
`ati audit tail` and `ati audit search` print JSON only when `--output json`; otherwise they emit one human-readable line per entry (`timestamp [OK|ERR] tool (Nms) agent=…`).
</Note>

### Error output and exit codes

On failure, `main` logs via `tracing::error!`. When output is JSON, stderr receives a structured object from `core::error::format_structured_error` with `code`, `message`, and `exit_code`. Exit codes are grouped by prefix: `input` → 2, `auth` → 3, `provider` → 4, `rate` → 5, default → 1.

## Command map

```text
ati [--output] [--verbose] [-J]
├── run <tool> [--arg value ...]
├── tool | tools
│   ├── list [--provider NAME]
│   ├── info <name>
│   └── search <query>
├── skill | skills
│   ├── list | show | search | info | install | remove | init | validate
│   ├── read | resolve | verify | diff | update
│   └── fetch <skillati-subcommand>
├── skillati
│   ├── catalog | read | resources | cat | refs | ref | build-index
├── assist [--plan] [--save FILE] [--local] <args...>
├── plan execute <file> [--confirm-each]
├── provider
│   ├── add-mcp | add-cli | import-openapi | inspect-openapi
│   ├── list | remove | info | load | install-skills | unload
├── auth status
├── token keygen | issue | inspect | validate
├── init [--proxy] [--es256]
├── key set | list | remove
├── audit tail | search
├── edge bootstrap-keyring | rotate-keyring
└── proxy [--port] [--bind] [--ati-dir] [--env-keys] [--migrate]
         [--enable-passthrough] [--sig-verify-mode] [--sig-drift-seconds]
         [--sig-exempt-paths]
```

<Info>
`ati init` is a top-level command (not nested). It creates `manifests/`, `specs/`, `skills/`, and `config.toml` under the ATI directory. Use `ati init --proxy` to seed JWT proxy configuration (HS256 by default, `--es256` for a key pair).
</Info>

## Runtime directory

Unless overridden by `ATI_DIR`, the ATI home defaults to `~/.ati` (`core/dirs.rs`). First use creates `manifests/`, `specs/`, `skills/`, and a stub `config.toml`.

| Path | Purpose |
|------|---------|
| `manifests/*.toml` | Provider and tool definitions |
| `specs/` | Downloaded OpenAPI specs |
| `skills/` | Installed skill directories |
| `credentials` | Dev-mode API keys (`ati key set`) |
| `keyring.enc` | AES-256-GCM encrypted secrets (local/proxy execution) |
| `audit.jsonl` | Tool-call audit log (override with `ATI_AUDIT_FILE`) |

## Execution mode auto-detection

Several commands branch on **`ATI_PROXY_URL`**:

| Command | Local (no proxy URL) | Proxy (`ATI_PROXY_URL` set) |
|---------|----------------------|-----------------------------|
| `ati run` | Keyring + direct HTTP/MCP/CLI dispatch | POST `/call` via `proxy/client.rs` |
| `ati assist` | Local LLM + registry context | POST `/help` |
| `ati tool *` | Local registry + scopes | Proxy tool endpoints |
| `ati skill *` / `skillati` | Local or GCS registry | Proxy `/skills` and `/skillati/*` |

`file_manager:*` tools always perform client-side file I/O first, then dispatch locally or to the proxy.

## `ati run`

Execute a named tool with `--key value` pairs captured as trailing arguments.

```bash
ati run finnhub:quote --symbol AAPL
ati run github:search_repositories --query "ati" -J
ati run web_search --query "rust async" --output table
```

<Steps>
<Step title="Parse arguments">
`parse_tool_args` converts `--key value` into a `HashMap<String, Value>`. Values are JSON-parsed when possible; bare `--flag` becomes `true`. Global flags `-J`, `--json`, `--verbose`, and `--output` are stripped if swallowed by `trailing_var_arg`.
</Step>
<Step title="Normalize keys">
`normalize_arg_keys` maps CLI keys to schema property names (case, hyphen, snake_case variants).
</Step>
<Step title="Dispatch">
Handler comes from the provider manifest: `mcp`, `cli`, or default HTTP/OpenAPI via `providers/generic`. Scope and rate limits apply when JWT validation is configured.
</Step>
<Step title="Format result">
Success prints formatted output to stdout. Audit entries append to `audit.jsonl` in local mode.
</Step>
</Steps>

<Warning>
`ati run` with a CLI provider passes **raw** `raw_args` to the subprocess executor so subcommand flags like `--help` reach the underlying binary unchanged.
</Warning>

## `ati tool`

Scope-filtered tool discovery (alias: `tools`).

| Subcommand | Arguments | Description |
|------------|-----------|-------------|
| `list` | `[--provider NAME]` | Tabular or JSON list of in-scope tools |
| `info` | `<name>` | Schema, provider, tags, and linked skills |
| `search` | `<query>` | Fuzzy match on name, description, tags, category |

## `ati provider`

Unified provider lifecycle: write manifests under `manifests/`, discover tools from OpenAPI or MCP, or register CLI binaries.

| Subcommand | Purpose |
|------------|---------|
| `add-mcp` | Generate MCP manifest (`--transport http\|stdio`, `--url` or `--command`, `--env KEY=VALUE`) |
| `add-cli` | Register a local CLI binary (`--command`, `--env`, `--timeout`) |
| `import-openapi` | Download spec to `specs/`, emit manifest (`--name`, `--auth-key`, `--include-tags`, `--dry-run`) |
| `inspect-openapi` | Preview operations and auth without saving |
| `list` / `info` / `remove` | Inspect or delete manifests |
| `load` | Ephemeral provider from OpenAPI URL or `--mcp` (`--save` to persist, `--ttl` cache) |
| `install-skills` | Install skills declared in a provider manifest |
| `unload` | Drop a cached ephemeral provider |

MCP and OpenAPI tools are namespaced as **`provider:tool`**.

## `ati skill` and `ati skillati`

**`ati skill`** (alias `skills`) manages methodology docs under `~/.ati/skills/`.

| Subcommand | Notes |
|------------|-------|
| `list` | Filters: `--category`, `--provider`, `--tool` |
| `show` / `info` | SKILL.md body vs `skill.toml` metadata (`--meta`, `--refs`) |
| `search` | Fuzzy over name, description, keywords, tools |
| `install` | Local path, git, or HTTPS (`--name`, `--all`, `--local` for offline manifest gen) |
| `remove` / `init` / `validate` | Lifecycle and validation (`--check-tools`) |
| `read` | Agent-oriented SKILL.md (`--tool`, `--with-refs`) |
| `resolve` | Skills auto-loaded for current scopes (`--scopes` path override) |
| `verify` / `diff` / `update` | Integrity and sync from source |
| `fetch` | Delegates to SkillATI subcommands (same as top-level `skillati`) |

**`ati skillati`** (also `ati skill fetch …`) reads the remote GCS skill registry without installing:

| Subcommand | Purpose |
|------------|---------|
| `catalog` | List remote skills (`--search`) |
| `read` | SKILL.md for one skill |
| `resources` | List bundled paths (`--prefix`) |
| `cat` | Read `scripts/`, `references/`, or cross-skill paths |
| `refs` / `ref` | Reference file listing and fetch |
| `build-index` | Build catalog manifest JSON for publishing (`--output-file`) |

## `ati assist`

LLM-powered discovery: natural-language query → recommended `ati run` commands.

```bash
ati assist "research Apple stock price and insiders"
ati assist finnhub "quote and sentiment for AAPL"
ati assist --plan --save plan.json "multi-step Finnhub research"
ati assist --local "offline ollama assist"
```

| Flag | Effect |
|------|--------|
| `--plan` | Structured JSON plan of tool steps (no prose) |
| `--save FILE` | Implies `--plan`; writes plan JSON |
| `--local` | Use local LLM (ollama) instead of hosted API |

If the first positional argument matches a tool or provider name, assist scopes to that surface.

## `ati plan`

| Subcommand | Description |
|------------|-------------|
| `execute FILE` | Run a saved plan JSON (`query`, `steps[]` with `tool`, `args`, `description`) |

`--confirm-each` prompts on a TTY before each step; failures can abort or continue interactively. Plans are produced by `ati assist --plan` / `--save`.

## `ati key`

Dev-oriented credential store at **`~/.ati/credentials`** (JSON map, mode `0600` on Unix). Distinct from **`keyring.enc`** used during tool execution.

| Subcommand | Syntax |
|------------|--------|
| `set` | `ati key set <name> <value>` |
| `list` | Masked values (`sk-1…cdef` style) |
| `remove` | `ati key remove <name>` |

## `ati token`

JWT lifecycle for proxy sandboxes (does not require `--output`).

| Subcommand | Key options |
|------------|-------------|
| `keygen` | `--algorithm ES256` (default) or `HS256` — prints PEM or hex secret |
| `issue` | `--sub`, `--scope`, `--ttl` (default 1800s), `--aud`, `--iss`, `--key` or `--secret`, repeatable `--rate` |
| `inspect` | Decode without verification |
| `validate` | Full verify (`--key` or `--secret`) |

## `ati auth`

| Subcommand | Description |
|------------|-------------|
| `status` | Decode `ATI_SESSION_TOKEN`: agent, scopes, expiry, verification status |

When no token is set and JWT is not configured locally, reports unrestricted dev mode.

## `ati audit`

Reads **`~/.ati/audit.jsonl`** (or `ATI_AUDIT_FILE`).

| Subcommand | Options |
|------------|---------|
| `tail` | `-n` / `--` count (default 20) |
| `search` | `--tool` (wildcard suffix), `--since` (`1h`, `30m`, `7d`) |

JSON output serializes full `AuditEntry` objects (`ts`, `tool`, `args`, `status`, `duration_ms`, `agent_sub`, optional `job_id`, `sandbox_id`, `error`).

## `ati edge`

Operator commands for edge VMs with 1Password-backed keyrings.

### `bootstrap-keyring`

Pulls labeled fields from `op item get`, writes `<ati_dir>/keyring.enc` and `<ati_dir>/.keyring-key` (persistent session key, idempotent).

| Flag | Description |
|------|-------------|
| `--vault` | 1Password vault name |
| `--item` | Item name or UUID |
| `--ati-dir` | ATI root (default `~/.ati`) |
| `--op-path` | `op` binary path |
| `--op-token-file` | Service-account token file → `OP_SERVICE_ACCOUNT_TOKEN` |

### `rotate-keyring`

Atomically replaces `keyring.enc`, then **SIGHUP** the systemd service (default `ati`) so the proxy reloads secrets without restart.

| Flag | Description |
|------|-------------|
| `--service` | Unit to signal (default `ati`) |
| `--no-signal` | Skip SIGHUP (pre-start rotation) |

Requires prior `bootstrap-keyring`. Field labels in 1Password become keyring entry names (for example `sandbox_signing_shared_secret`).

## `ati proxy`

Run ATI as an HTTP server holding secrets for sandboxed agents.

```bash
ati proxy --port 8090 --bind 127.0.0.1
ati proxy --env-keys --migrate
ati proxy --enable-passthrough --sig-verify-mode enforce
```

| Flag | Default | Description |
|------|---------|-------------|
| `--port` | `8090` | Listen port |
| `--bind` | `127.0.0.1` | Bind address (`0.0.0.0` for all interfaces) |
| `--ati-dir` | `~/.ati` | Manifests, keyring, scopes |
| `--env-keys` | off | Load API keys from environment instead of `keyring.enc` |
| `--migrate` | off | Apply embedded DB migrations when `ATI_DB_URL` is set |
| `--enable-passthrough` | off | Expose raw HTTP routes from `handler = "passthrough"` manifests |
| `--sig-verify-mode` | `log` | HMAC sandbox signature: `log`, `warn`, or `enforce` |
| `--sig-drift-seconds` | `60` | Max clock skew for signed requests |
| `--sig-exempt-paths` | built-in set | Comma-separated path globs exempt from verification |

<Note>
Signing secret is read from keyring entry `sandbox_signing_shared_secret`. In `enforce` mode, a missing secret fails closed at startup.
</Note>

Proxy routes (`/call`, `/mcp`, `/help`, `/skills`, `/skillati/*`, `/health`, JWKS, optional admin) are documented on the proxy API page.

## Local scopes and JWT

`cli::common::load_local_scopes_from_env` enforces:

- If JWT validation env is configured → **`ATI_SESSION_TOKEN`** (or file variants) required and validated.
- Otherwise → unrestricted dev mode for local commands.

This affects `ati tool list`, `ati run` (local path), and skill visibility gates.

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
Initialize `~/.ati/`, import a provider, store keys, and run your first tool.
</Card>
<Card title="Execution modes" href="/execution-modes">
Local keyring vs `ATI_PROXY_URL` proxy mode and credential placement.
</Card>
<Card title="Environment variables" href="/environment-variables">
`ATI_OUTPUT`, `ATI_DIR`, `ATI_PROXY_URL`, `ATI_SESSION_TOKEN`, JWT keys, and audit paths.
</Card>
<Card title="Assist and plan reference" href="/assist-and-plan-reference">
`--plan`, `--save`, internal `_llm` provider, and plan JSON schema.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
`ati token`, `ati init --proxy`, and session token issuance patterns.
</Card>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
Production `ati proxy` binding, passthrough, sig-verify rollout, and systemd.
</Card>
</CardGroup>

---

## 16. Environment variables

> Runtime env contract: `ATI_PROXY_URL`, `ATI_SESSION_TOKEN`, `ATI_DIR`, JWT keys, SSRF/download allowlists, skill registry, OTel export, and optional `ATI_DB_URL` / `ATI_ADMIN_TOKEN`.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/16-environment-variables.md
- Generated: 2026-06-02T03:17:52.489Z

### Source Files

- `README.md`
- `AGENTS.md`
- `src/main.rs`
- `src/proxy/server.rs`
- `docs/OTEL.md`
- `docs/PERSISTENCE.md`
- `src/core/otel.rs`

---
title: "Environment variables"
description: "Runtime env contract: `ATI_PROXY_URL`, `ATI_SESSION_TOKEN`, `ATI_DIR`, JWT keys, SSRF/download allowlists, skill registry, OTel export, and optional `ATI_DB_URL` / `ATI_ADMIN_TOKEN`."
---

ATI reads process environment variables at startup and on each CLI invocation to choose execution mode (local vs proxy), resolve filesystem paths, authenticate proxy traffic, load operator credentials, gate outbound fetches, and optionally export telemetry or connect to Postgres. Most variables are optional; unset values fall back to documented defaults or disable the feature entirely.

```mermaid
flowchart TB
  subgraph sandbox["Agent sandbox / CLI"]
    RUN["ati run / assist / skill"]
    ENV["ATI_PROXY_URL\nATI_SESSION_TOKEN*\nATI_DIR"]
  end
  subgraph proxy["ati proxy"]
    JWT["ATI_JWT_*"]
    KEYS["ATI_KEY_*"]
    SEC["ATI_SSRF_PROTECTION\nATI_DOWNLOAD_ALLOWLIST"]
    DB["ATI_DB_URL\nATI_ADMIN_TOKEN"]
    SK["ATI_SKILL_REGISTRY"]
    OTEL["OTEL_EXPORTER_OTLP_*"]
  end
  RUN -->|"ATI_PROXY_URL set"| proxy
  RUN -->|"unset"| LOCAL["Local keyring\n~/.ati or ATI_DIR"]
  ENV --> RUN
  JWT --> RUN
  KEYS --> proxy
```

## Mode and filesystem layout

| Variable | Read by | Default | Effect |
|----------|---------|---------|--------|
| `ATI_PROXY_URL` | CLI (`ati run`, `ati assist`, skill fetch in proxy registry mode) | unset | When set and non-empty, tool calls forward to `{url}/call` (and related proxy routes). When unset, ATI uses local mode with manifests and keyring under `ATI_DIR`. |
| `ATI_DIR` | CLI, proxy (`--ati-dir` overrides for proxy only) | `$HOME/.ati`, else `.ati` | Root for `manifests/`, `keyring.enc`, `credentials`, `skills/`, provider cache, and default audit path. |
| `ATI_KEY_FILE` | Local mode session-key read (`security/sealed_file.rs`) | `/run/ati/.key` | Path to the one-shot AES session key file; read once then unlinked. Used in tests and custom orchestrator layouts. |
| `ATI_OUTPUT` | Global CLI flag (`main.rs`) | `json` (clap default) | Default for `--output` / `--format`: `json`, `table`, or `text`. |
| `ATI_CLI_MAX_OUTPUT_BYTES` | CLI tool capture (`core/cli_executor.rs`) | `524288000` (500 MiB) | Per-file cap on subprocess output captured by CLI providers; parse errors or `0` fall back to default. |
| `ATI_AUDIT_FILE` | `ati audit` (`core/audit.rs`) | `{ATI_DIR}/audit.jsonl` | Append-only JSONL audit log path. |

<Note>
`ati proxy` takes `--ati-dir` on the command line; sandboxes normally set `ATI_DIR` so every `ati` subprocess shares the same config root.
</Note>

## Proxy authentication and session tokens

Proxy-mode clients attach `Authorization: Bearer <token>` on `/call`, `/mcp`, `/help`, and SkillATI routes. Token material is **not** cached across invocations when read from a file—each call re-reads disk so supervisors can rotate JWTs without restarting agents.

### Resolution order (any `*_SESSION_TOKEN` env name)

For env var `<NAME>` (default `ATI_SESSION_TOKEN`):

1. `<NAME>` if set and non-empty (trimmed)
2. `<NAME>_FILE` if set and non-empty → read that path
3. Default file path from `<NAME>` (see below)

File read errors (permissions) return `Err`; missing file returns `Ok(None)`.

| Variable | Role |
|----------|------|
| `ATI_SESSION_TOKEN` | Default bearer JWT for proxy auth; carries space-delimited scopes in the `scope` claim. |
| `ATI_SESSION_TOKEN_FILE` | Override path for token file; when unset, default is `/run/ati/session_token`. |
| Per-provider env (manifest) | `[provider].auth_session_token_env` names a sandbox env var (e.g. `PARCHA_TOOLS_SESSION_TOKEN`) for audience-separated tokens. Same `<NAME>_FILE` and `/run/ati/<slug>` default path rules apply. If the per-provider token is missing, the client falls back to `ATI_SESSION_TOKEN` before sending an unauthenticated request. |

<Warning>
When the proxy has JWT validation configured (`ATI_JWT_PUBLIC_KEY` or `ATI_JWT_SECRET`), missing or invalid tokens are rejected. With no JWT keys configured, the proxy runs in unrestricted dev mode—do not expose that configuration to untrusted networks.
</Warning>

## JWT validation and issuance (proxy)

Configured on the **proxy** process. `jwt::config_from_env()` builds validation settings; absence of both key sources means JWT auth is disabled.

| Variable | Default | Effect |
|----------|---------|--------|
| `ATI_JWT_PUBLIC_KEY` | unset | Path to ES256 public key PEM; enables JWT validation and JWKS endpoint when combined with private key. |
| `ATI_JWT_PRIVATE_KEY` | unset | Path to ES256 private key PEM (token issuance / JWKS). |
| `ATI_JWT_SECRET` | unset | Hex-encoded HS256 shared secret; used if no public key path is set. |
| `ATI_JWT_ISSUER` | unset | If set, JWT `iss` must match. |
| `ATI_JWT_AUDIENCE` | `ati-proxy` | Singular expected `aud` when CSV allowlist is not used. |
| `ATI_JWT_ACCEPTED_AUDIENCES` | unset | Comma-separated allowlist of `aud` values; takes precedence over `ATI_JWT_AUDIENCE` when non-empty after trimming. Used for per-provider audience separation with custom session-token env vars. |

## Proxy credential injection (`ATI_KEY_*`)

When `ati proxy` starts with `--env-keys`, the keyring is built exclusively from environment variables matching `ATI_KEY_*`. The prefix is stripped and the remainder is lowercased for the keyring name.

| Pattern | Example | Keyring key |
|---------|---------|-------------|
| `ATI_KEY_<NAME>` | `ATI_KEY_FINNHUB_API_KEY=...` | `finnhub_api_key` |
| `ATI_KEY_<PROVIDER>_ALLOWED_URLS` | `ATI_KEY_PARCHA_TOOLS_ALLOWED_URLS=*.example.com` | `{provider}_allowed_urls` (glob CSV) |

<ParamField body="ATI_KEY_*" type="string">
Used on the proxy with `--env-keys`, or in tests. Replaces disk keyring for that process. Empty values are skipped. MCP HTTP upstream overrides require a compiled allowlist for providers that declare `mcp_url_env`; without `ATI_KEY_<PROVIDER>_ALLOWED_URLS`, the proxy returns 403.
</ParamField>

Other notable keyring keys (set via `ati key set` or `ATI_KEY_*`):

| Keyring name | Purpose |
|--------------|---------|
| `gcp_credentials` | GCP service account JSON for `ATI_SKILL_REGISTRY=gcs://...` |
| `sandbox_signing_shared_secret` | HMAC request signing when passthrough sig-verify is enabled (`ATI_KEY_SANDBOX_SIGNING_SHARED_SECRET` under `--env-keys`) |

## Outbound safety: SSRF and downloads

| Variable | Values | Scope |
|----------|--------|-------|
| `ATI_SSRF_PROTECTION` | unset (off), `warn`, `1` / `true` | HTTP executor and file-manager downloads via `validate_url_not_private`. Blocks loopback, RFC1918, link-local, `.internal`, `.local`, and DNS-resolved private IPs. |
| `ATI_DOWNLOAD_ALLOWLIST` | Comma-separated host patterns | `file_manager:download` only. Unset = no host restriction (still subject to SSRF when enabled). Patterns: exact host, `*.suffix`, or `*` (not recommended). |

<Warning>
Production proxies should set `ATI_DOWNLOAD_ALLOWLIST`. When unset, any non-private host is allowed for downloads—acceptable for local dev only.
</Warning>

## Skill registry (SkillATI)

| Variable | Values | Effect |
|----------|--------|--------|
| `ATI_SKILL_REGISTRY` | unset, `gcs://<bucket>`, `proxy` | Enables remote SkillATI. `gcs://` reads from GCS using keyring key `gcp_credentials`. `proxy` fetches `/skillati/*` through `ATI_PROXY_URL` with session token auth. |
| `ATI_SKILL_REGISTRY_INDEX_OBJECT` | Comma-separated object paths | Overrides GCS catalog index candidates (default tries `_skillati/catalog.v1.json` and fallbacks). |
| `ATI_PROXY_URL` | URL | Required when `ATI_SKILL_REGISTRY=proxy`. |
| `ATI_SESSION_TOKEN` | JWT | Auth for proxy-backed SkillATI client. |

Local skills under `{ATI_DIR}/skills/` load regardless of registry settings.

## Optional persistence and admin API

Requires a binary built with `--features db`.

| Variable | Default | Effect |
|----------|---------|--------|
| `ATI_DB_URL` | unset | Postgres connection string. Unset or empty → persistence disabled, `/health` reports `db: "disabled"`. Set with unreachable DB → proxy **fails startup**. Set without `db` feature → startup error instructing rebuild. |
| `ATI_ADMIN_TOKEN` | unset | Plain-text bearer for `/admin/keys/*`. Required alongside a connected DB; if DB is connected but token unset, admin routes return 503. |
| `ATI_MASTER_KEY` | unset | Base64 32-byte KEK for envelope encryption of secrets at rest (persistence layer). |
| `ATI_MASTER_KEY_<id>` | unset | Versioned KEKs for rotation. |
| `ATI_MASTER_KEY_ACTIVE` | `m1` when multi-version | Selects which KEK version encrypts new writes. |

Use `ati proxy --migrate` to apply embedded SQL migrations when `ATI_DB_URL` is set.

## Observability

### Logging

| Variable | Default | Effect |
|----------|---------|--------|
| `RUST_LOG` | `info` (or `debug` with `--verbose`) | `tracing` filter via `EnvFilter::from_default_env()`. |

### OpenTelemetry (`--features otel`)

Runtime export is active only when `OTEL_EXPORTER_OTLP_ENDPOINT` is set (non-empty). See `docs/OTEL.md` for full detail.

| Variable | Default | Notes |
|----------|---------|-------|
| `OTEL_EXPORTER_OTLP_ENDPOINT` | unset | Base OTLP HTTP endpoint; unset disables OTel layer at runtime. |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `{base}/v1/traces` | Signal-specific override. |
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | `{base}/v1/metrics` | Signal-specific override. |
| `OTEL_EXPORTER_OTLP_HEADERS` | unset | Comma-separated `k=v` (e.g. Grafana `Authorization=Basic ...`). |
| `OTEL_SERVICE_NAME` | `ati-proxy` | Resource attribute; wins over `service.name` in `OTEL_RESOURCE_ATTRIBUTES`. |
| `OTEL_RESOURCE_ATTRIBUTES` | unset | Lower-priority resource defaults. |
| `ENVIRONMENT_TIER` | unset | Maps to `deployment.environment` on OTel resource and Sentry environment. |
| `OTEL_TRACES_SAMPLER` / `OTEL_TRACES_SAMPLER_ARG` | SDK defaults | Standard OTel sampling env vars. |
| `ATI_OTEL_DEBUG` | `false` | Reserved; no-op today. |

### Sentry (`--features sentry`)

| Variable | Default | Effect |
|----------|---------|--------|
| `SENTRY_DSN` or `GREP_SENTRY_DSN` | unset | Enables Sentry when `ENVIRONMENT_TIER` is `production`, `staging`, or `demo`. |
| `SERVICE_NAME` | `ati-proxy` | Sentry server name. |
| `ATI_SENTRY_DEBUG` | `false` | `1` / `true` enables Sentry client debug mode. |

<Info>
If `OTEL_EXPORTER_OTLP_ENDPOINT` or `SENTRY_DSN` is set but the matching Cargo feature was not compiled in, ATI logs a startup warning and ignores export.
</Info>

## Typical deployment profiles

<Steps>
<Step title="Local developer laptop">
Unset `ATI_PROXY_URL`. Optionally set `ATI_DIR` to a project-local tree. Leave `ATI_SSRF_PROTECTION` and `ATI_DOWNLOAD_ALLOWLIST` unset for permissive HTTP. Store API keys with `ati key set` or `~/.ati/credentials`.
</Step>
<Step title="Sandbox with proxy (zero credentials in agent)">
Set `ATI_PROXY_URL`, `ATI_SESSION_TOKEN` (or rely on `/run/ati/session_token` rotation). Do **not** ship `keyring.enc` into the sandbox. Scope tools via JWT `scope` claim.
</Step>
<Step title="Production proxy host">
Run `ati proxy --env-keys` or disk keyring under `--ati-dir`. Configure `ATI_JWT_PUBLIC_KEY` + `ATI_JWT_ISSUER`, `ATI_DOWNLOAD_ALLOWLIST`, `ATI_SSRF_PROTECTION=1`, MCP allowlists via `ATI_KEY_*_ALLOWED_URLS`. Optionally `ATI_DB_URL`, `ATI_ADMIN_TOKEN`, `OTEL_*`, and `ATI_SKILL_REGISTRY=gcs://...`.
</Step>
</Steps>

## Build-time feature gates

| Cargo feature | Env vars requiring rebuild |
|---------------|----------------------------|
| `db` | `ATI_DB_URL`, `ATI_ADMIN_TOKEN`, `ATI_MASTER_KEY*` |
| `otel` | `OTEL_EXPORTER_OTLP_*` |
| `sentry` | `SENTRY_DSN`, `GREP_SENTRY_DSN` |

## Test-only variables

| Variable | Purpose |
|----------|---------|
| `ATI_DB_URL_TEST` | Integration tests against a live Postgres instance; skipped when unset. |

## Related pages

<CardGroup>
<Card title="Execution modes" href="/execution-modes">
How `ATI_PROXY_URL` switches local vs proxy execution and where credentials live.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
Issuing tokens, JWT env setup, and `ati key` workflows.
</Card>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
`--env-keys`, `--migrate`, passthrough, and production proxy flags.
</Card>
<Card title="File manager operations" href="/file-manager-operations">
Download allowlist semantics and SSRF interaction for `file_manager:download`.
</Card>
<Card title="Skills registry and fetch" href="/skills-registry-and-fetch">
GCS layout, `ATI_SKILL_REGISTRY`, and proxy SkillATI routes.
</Card>
<Card title="Security and production" href="/security-and-production">
Threat model, hardening checklist, and OTel/Sentry in prod builds.
</Card>
</CardGroup>

---

## 17. Proxy API reference

> HTTP routes on `ati proxy`: `/call`, `/mcp`, `/help`, `/tools`, `/skills`, `/skillati/*`, `/health`, JWKS, optional `/admin/keys/*`, auth requirements, and request/response shapes.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/17-proxy-api-reference.md
- Generated: 2026-06-02T03:18:42.391Z

### Source Files

- `src/proxy/server.rs`
- `src/proxy/client.rs`
- `README.md`
- `tests/proxy_test.rs`
- `tests/proxy_skills_test.rs`
- `scripts/test_proxy_server_e2e.sh`

---
title: "Proxy API reference"
description: "HTTP routes on `ati proxy`: `/call`, `/mcp`, `/help`, `/tools`, `/skills`, `/skillati/*`, `/health`, JWKS, optional `/admin/keys/*`, auth requirements, and request/response shapes."
---

`ati proxy` is an Axum HTTP server that holds manifests, the encrypted keyring, optional Postgres state, and optional passthrough routes. Sandboxed agents set `ATI_PROXY_URL` and call these endpoints; the proxy injects credentials, enforces JWT or virtual-key scopes, and returns JSON. Named routes are registered in `build_router`; unmatched paths can fall through to `handler = "passthrough"` manifests when `--enable-passthrough` is set.

```mermaid
sequenceDiagram
    participant Agent as Sandbox agent
    participant Proxy as ati proxy
    participant Sig as sig_verify middleware
    participant Auth as auth_middleware
    participant Up as Upstream API or MCP

    Agent->>Proxy: POST /call + Bearer JWT
    Proxy->>Sig: HMAC check (mode-dependent)
    Sig->>Auth: JWT or Ati-Key
    Auth->>Proxy: TokenClaims in extensions
    Proxy->>Proxy: Scope + rate limit
    Proxy->>Up: HTTP / MCP / CLI / file_manager
    Up-->>Proxy: Raw response
    Proxy-->>Agent: {"result": ..., "error": null}
```

## Authentication

Requests pass through **HMAC signature verification** (`sig_verify_middleware`, outermost) then **JWT / virtual-key auth** (`auth_middleware`). Public routes skip JWT only; they still run sig-verify unless the path is exempt.

| Mechanism | Header | When | Behavior |
|-----------|--------|------|----------|
| JWT (default) | `Authorization: Bearer <jwt>` | `jwt_config` present at startup | Validated with `ATI_JWT_PUBLIC_KEY` or `ATI_JWT_SECRET`; claims stored in request extensions |
| Virtual key | `Authorization: Ati-Key <raw>` | `ATI_DB_URL` + `db` feature | SHA-256 hash lookup; synthetic `TokenClaims` from row scopes |
| Admin master token | `Authorization: Bearer <ATI_ADMIN_TOKEN>` | `/admin/keys/*` only | Separate router; constant-time compare |
| Dev mode | (none) | No JWT keys configured | Named routes allowed without bearer; scopes unrestricted |
| Passthrough | HMAC only | Non-named host+path match + `--enable-passthrough` | JWT skipped; identity is HMAC-signed sandbox |

<ParamField header="Authorization" type="string">
Bearer JWT or `Ati-Key` raw token. Required on all named routes when JWT is configured. Scopes live in the token, not the request body.
</ParamField>

<ParamField header="X-Sandbox-Signature" type="string">
HMAC payload `t=<unix>,s=<hex>` where message is `{t}.{METHOD}.{path}`. Secret from keyring entry `sandbox_signing_shared_secret`. Controlled by `--sig-verify-mode` (`log` default, `warn`, `enforce`).
</ParamField>

<ParamField header="X-Ati-Upstream-Url" type="string">
Optional per-request MCP upstream override. Proxy validates against operator allowlist `{provider}_allowed_urls` in the keyring. Used with provider `mcp_url_env`.
</ParamField>

<Note>
`GET /health` and `GET /.well-known/jwks.json` skip JWT. Default sig-verify exempt paths include `/health`, `/.well-known/jwks.json`, plus operator overrides via `--sig-exempt-paths`.
</Note>

## Endpoint inventory

| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET | `/health` | None | Liveness and counts |
| GET | `/.well-known/jwks.json` | None | ES256 public JWKS (when configured) |
| POST | `/call` | JWT / Ati-Key / dev | Execute a tool |
| POST | `/mcp` | JWT / Ati-Key / dev | MCP JSON-RPC gateway |
| POST | `/help` | JWT / Ati-Key / dev | LLM tool discovery assist |
| GET | `/tools` | JWT / Ati-Key / dev | List scope-visible tools |
| GET | `/tools/{name}` | JWT / Ati-Key / dev | Tool detail |
| GET | `/skills` | JWT / Ati-Key / dev | List local skills |
| GET | `/skills/{name}` | JWT / Ati-Key / dev | Skill body or metadata |
| GET | `/skills/{name}/bundle` | JWT / Ati-Key / dev | Full skill directory |
| POST | `/skills/resolve` | JWT / Ati-Key / dev | Resolve skills from scope list |
| POST | `/skills/bundle` | JWT / Ati-Key / dev | Batch skill bundles |
| GET | `/skillati/catalog` | JWT / Ati-Key / dev | Remote GCS catalog |
| GET | `/skillati/{name}` | JWT / Ati-Key / dev | Level-2 SKILL.md activation |
| GET | `/skillati/{name}/resources` | JWT / Ati-Key / dev | List remote resources |
| GET | `/skillati/{name}/file` | JWT / Ati-Key / dev | Read remote file |
| GET | `/skillati/{name}/refs` | JWT / Ati-Key / dev | List references |
| GET | `/skillati/{name}/ref/{reference}` | JWT / Ati-Key / dev | Read reference |
| POST | `/admin/keys/issue` | Admin bearer | Issue virtual key (`db` feature) |
| GET | `/admin/keys` | Admin bearer | List keys for `user_id` |
| GET | `/admin/keys/{hash}` | Admin bearer | Key metadata |
| DELETE | `/admin/keys/{hash}` | Admin bearer | Revoke key |
| POST | `/admin/keys/bulk-revoke` | Admin bearer | Bulk revoke |

## Public endpoints

:::endpoint GET /health
Proxy liveness and configuration summary. No authentication.
:::

<ResponseField name="status" type="string">
Always `"ok"` when the process is serving.
</ResponseField>

<ResponseField name="version" type="string">
Crate version from `CARGO_PKG_VERSION`.
</ResponseField>

<ResponseField name="tools" type="number">
Count of public manifest tools.
</ResponseField>

<ResponseField name="providers" type="number">
Provider count.
</ResponseField>

<ResponseField name="skills" type="number">
Local `SkillRegistry` skill count.
</ResponseField>

<ResponseField name="auth" type="string">
`"jwt"` when JWT validation is enabled; `"disabled"` in dev mode.
</ResponseField>

<ResponseField name="db" type="string">
`"disabled"` or `"connected"` depending on `ATI_DB_URL` and pool state.
</ResponseField>

<RequestExample>

```bash
curl -s http://127.0.0.1:8090/health
```

</RequestExample>

<ResponseExample>

```json
{
  "status": "ok",
  "version": "0.1.0",
  "tools": 42,
  "providers": 8,
  "skills": 3,
  "auth": "jwt",
  "db": "disabled"
}
```

</ResponseExample>

:::endpoint GET /.well-known/jwks.json
JWKS document for ES256 JWT validation. Precomputed at startup from `ATI_JWT_PUBLIC_KEY`.
:::

| Status | Body |
|--------|------|
| 200 | JWKS JSON (`keys` array) |
| 404 | `{"error": "JWKS not configured"}` |

## Tool execution

:::endpoint POST /call
Execute one tool by name. Dispatches on provider `handler`: `mcp`, `cli`, `file_manager`, or default HTTP/OpenAPI path via `core/http`.
:::

<ParamField body="tool_name" type="string" required>
Manifest tool name, e.g. `finnhub:quote` or `mock_search`. Underscore names are auto-resolved to colon form (`finnhub_quote` → `finnhub:quote`).
</ParamField>

<ParamField body="args" type="object | array | string" required={false}>
HTTP/MCP/OpenAPI: JSON object of parameters (default `{}`). CLI: JSON array of positional strings, a single whitespace-separated string, or `{"_positional": [...]}`.
</ParamField>

<ParamField body="raw_args" type="string[]" required={false}>
Deprecated CLI path; when set, takes precedence over `args` for positional parsing.
</ParamField>

<ResponseField name="result" type="object">
Processed tool output (may be upstream JSON after `response.extract` / `format`).
</ResponseField>

<ResponseField name="error" type="string">
Human-readable failure; omitted on success.
</ResponseField>

| HTTP status | Typical cause |
|-------------|----------------|
| 200 | Success (`error` may still be set in body for logical failures returned as 200 from client) |
| 400 | Body read failure; invalid `X-Ati-Upstream-Url` without `mcp_url_env` |
| 403 | Missing JWT; scope denial; upstream URL not on allowlist |
| 404 | Unknown tool |
| 422 | Invalid JSON body |
| 429 | Rate limit from JWT `ati` rate claims |
| 502 | MCP/CLI/HTTP upstream failure |
| 500 | Response processing error (raw upstream kept in `result`) |

<Warning>
`POST /call` body limit is ~1.37 GiB (derived from `file_manager::MAX_UPLOAD_BYTES` base64 overhead). Large uploads still hit per-tool `MAX_UPLOAD_BYTES` (1 GiB) inside the handler.
</Warning>

<RequestExample>

```bash
export ATI_PROXY_URL=http://127.0.0.1:8090
export ATI_SESSION_TOKEN=eyJ...

curl -s -X POST "$ATI_PROXY_URL/call" \
  -H "Authorization: Bearer $ATI_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tool_name":"mock_search","args":{"query":"hello"}}'
```

</RequestExample>

<ResponseExample>

```json
{
  "result": {
    "results": [{"title": "Result for: hello", "score": 0.95}],
    "total": 2
  }
}
```

</ResponseExample>

## MCP JSON-RPC

:::endpoint POST /mcp
MCP protocol gateway. Accepts a single JSON-RPC 2.0 object; returns JSON-RPC envelope (HTTP 200 even for JSON-RPC errors).
:::

Supported methods:

| Method | Behavior |
|--------|----------|
| `initialize` | Returns protocol `2025-03-26`, server `ati-proxy` |
| `notifications/initialized` | HTTP 202, null body |
| `tools/list` | Scope-filtered tools with `inputSchema` |
| `tools/call` | Executes tool; MCP providers use native MCP; CLI/HTTP bridged |

JSON-RPC error codes used by the proxy:

| Code | Meaning |
|------|---------|
| -32601 | Unknown method |
| -32602 | Invalid params (e.g. missing tool name, bad upstream header) |
| -32001 | Scope or allowlist denial |

`tools/call` success wraps non-string JSON as pretty-printed text in `content[0].text`.

## Help (LLM assist)

:::endpoint POST /help
LLM-powered discovery over scope-visible tools and resolved skills. Requires internal `_llm` provider (`_chat_completion` tool) and keyring API key.
:::

<ParamField body="query" type="string" required>
User question for the assist model.
</ParamField>

<ParamField body="tool" type="string" required={false}>
When set, narrows the system prompt to that tool or provider; returns 403 if not visible in caller scopes.
</ParamField>

<ResponseField name="content" type="string">
Model completion text (from `/choices/0/message/content`).
</ResponseField>

<ResponseField name="error" type="string">
Configuration or upstream failure message.
</ResponseField>

<Info>
The handler uses model `zai-glm-4.7` with `max_completion_tokens: 1536` and `temperature: 0.3`. Include `help` in JWT scopes when issuing tokens if you want agents to use assist consistently (see scope reference).
</Info>

## Tool discovery

:::endpoint GET /tools
List tools visible to the caller's JWT scopes.
:::

Query parameters: `provider`, `search` (matches name, description, tags).

Response: JSON array of `{ name, description, provider, method, tags, skills, input_schema }`.

:::endpoint GET /tools/{name}
Detailed tool record including `endpoint`, `hint`, merged `skills`, and `scope`.
:::

Returns 404 when the tool is missing or not allowed by scopes.

## Local skills (`/skills`)

Skills come from `~/.ati/skills/` (or `--ati-dir`). All routes filter by `visible_skill_names` derived from JWT scopes.

:::endpoint GET /skills
List installed skills. Query: `category`, `provider`, `tool`, `search`.
:::

:::endpoint GET /skills/{name}
Default: `{ name, version, description, content }` (SKILL.md body). `?meta=true` returns metadata only. `?refs=true` adds `references` array.
:::

:::endpoint GET /skills/{name}/bundle
All files under the skill directory. Text files as strings; binary as `{ "base64": "..." }`.
:::

:::endpoint POST /skills/resolve
Resolve skills for an explicit scope list (does not expand caller JWT scopes into the request—intersection with caller visibility still applies).
:::

<ParamField body="scopes" type="string[]" required>
Scope strings, e.g. `["tool:finnhub:*", "skill:financial-analysis"]`.
</ParamField>

<ParamField body="include_content" type="boolean" required={false}>
When true, embed SKILL.md `content` in each resolved entry.
</ParamField>

:::endpoint POST /skills/bundle
Batch fetch bundles for up to 50 skill names.
:::

<ParamField body="names" type="string[]" required>
Skill names to bundle.
</ParamField>

Response: `{ "skills": { "<name>": { "files": { ... } } }, "missing": ["..."] }`.

## Remote SkillATI (`/skillati`)

Requires `ATI_SKILL_REGISTRY` (e.g. `gcs://bucket` on the proxy host). Catalog and files are fetched lazily; results are scope-filtered like local skills.

:::endpoint GET /skillati/catalog
Remote skill catalog. Optional `?search=` (filtered to 25 matches).
:::

Response: `{ "skills": [ RemoteSkillMeta, ... ] }` where each entry includes `name`, `description`, `skill_directory`, optional `when_to_use`, `keywords`, `tools`, `providers`, `categories`.

:::endpoint GET /skillati/{name}
Level-2 activation payload (`SkillAtiActivation`): `name`, `description`, `skill_directory`, `content` (SKILL.md with `${ATI_SKILL_DIR}` / cross-skill refs rewritten to `skillati://` URIs).
:::

:::endpoint GET /skillati/{name}/resources
List resource paths. Query: `prefix`.
:::

:::endpoint GET /skillati/{name}/file
Read one file. Query: `path` (required).
:::

:::endpoint GET /skillati/{name}/refs
List reference documents.
:::

:::endpoint GET /skillati/{name}/ref/{reference}
Read one reference by name.
:::

SkillATI errors return `{ "error": "<message>" }` with status 503 (not configured), 404 (not found), 400 (invalid path), or 502 (GCS/proxy failure).

## Admin virtual keys (`/admin/keys`)

<Warning>
Requires `cargo build --features db`, `ATI_DB_URL`, and `ATI_ADMIN_TOKEN`. Without them, admin routes return 503 with `admin endpoints require ATI_DB_URL + ATI_ADMIN_TOKEN`.
</Warning>

Admin routes use a **separate** bearer (`ATI_ADMIN_TOKEN`), not agent JWTs.

:::endpoint POST /admin/keys/issue
Create a revocable virtual key. Returns 201 with `raw_key`, `hash`, `alias`, `expires_at`.
:::

<ParamField body="user_id" type="string" required>
Owner identifier for listing and audit.
</ParamField>

<ParamField body="alias" type="string" required>
Human-readable key alias.
</ParamField>

<ParamField body="tools" type="string[]" required={false}>
Allowed tool scopes stored on the key row.
</ParamField>

<ParamField body="providers" type="string[]" required={false}>
Allowed provider scopes.
</ParamField>

<ParamField body="categories" type="string[]" required={false}>
Allowed category scopes.
</ParamField>

<ParamField body="skills" type="string[]" required={false}>
Allowed skill scopes.
</ParamField>

<ParamField body="expires_in_secs" type="number" required={false}>
TTL from issuance time.
</ParamField>

:::endpoint GET /admin/keys?user_id={id}
List active sessions for a user. `user_id` query param required.
:::

:::endpoint GET /admin/keys/{hash}
Metadata for one key (no raw secret).
:::

:::endpoint DELETE /admin/keys/{hash}
Revoke one key. Optional `?by=` audit actor (default `admin`).
:::

:::endpoint POST /admin/keys/bulk-revoke
Revoke by `user_id`, `alias_prefix`, and/or `hashes[]`. Returns `{ "revoked": <count> }`.
:::

Agents use issued keys via `Authorization: Ati-Key <raw_key>` on standard proxy routes.

## Proxy client (`ATI_PROXY_URL`)

When `ATI_PROXY_URL` is set, the CLI proxy client forwards calls instead of using the local keyring:

| CLI action | HTTP call |
|------------|-----------|
| `ati run <tool>` | `POST /call` |
| MCP integrations | `POST /mcp` |
| `ati assist` (proxy mode) | `POST /help` |
| Skill listing | `GET /skills` |
| Skill resolve | `POST /skills/resolve` |

Token resolution: `ATI_SESSION_TOKEN`, `<NAME>_FILE`, or per-provider `auth_session_token_env` with fallback to `ATI_SESSION_TOKEN`. Timeout: 120 seconds.

## Passthrough fallback

With `--enable-passthrough`, unmatched host+path requests hit `core/passthrough::handle_passthrough` after named routes fail. Those requests **skip JWT** and rely on HMAC sig-verify plus manifest-defined upstream routing. Named ATI paths always require JWT when configured, even if a passthrough manifest would match a similar URL.

## Verification

<Steps>
<Step title="Start proxy">
Run `ati proxy --port 8090 --ati-dir ~/.ati` (add `--migrate` and `ATI_DB_URL` when using persistence).
</Step>
<Step title="Probe health">
`curl -sf http://127.0.0.1:8090/health` should return `"status":"ok"`.
</Step>
<Step title="Exercise /call">
Set `ATI_PROXY_URL` and `ATI_SESSION_TOKEN`, then `ati run <tool> --output json` or `bash scripts/test_proxy_server_e2e.sh` for a full client→proxy→upstream round-trip.
</Step>
</Steps>

## Related pages

<CardGroup>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
Bind address, `--env-keys`, passthrough, sig-verify modes, and production probes.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
Token issuance, JWKS setup, and session token env vars.
</Card>
<Card title="JWT and scopes reference" href="/jwt-scopes-reference">
Scope grammar, wildcards, and validation failures.
</Card>
<Card title="Skills and SkillATI" href="/skills-and-skillati">
Progressive disclosure and remote registry layout.
</Card>
<Card title="Environment variables" href="/environment-variables">
`ATI_PROXY_URL`, `ATI_SESSION_TOKEN`, `ATI_DB_URL`, `ATI_ADMIN_TOKEN`, and SkillATI registry vars.
</Card>
</CardGroup>

---

## 18. Manifest reference

> TOML schema for `[provider]` and `[[tools]]`: `handler`, auth types, MCP/OpenAPI/CLI/passthrough fields, response `extract`/`format`, overrides, and validation errors at load time.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/18-manifest-reference.md
- Generated: 2026-06-02T03:18:39.291Z

### Source Files

- `src/core/manifest.rs`
- `manifests/README.md`
- `manifests/finnhub.toml`
- `manifests/github-mcp.toml`
- `manifests/google-workspace.toml`
- `tests/manifest_test.rs`
- `src/core/response.rs`

---
title: "Manifest reference"
description: "TOML schema for `[provider]` and `[[tools]]`: `handler`, auth types, MCP/OpenAPI/CLI/passthrough fields, response `extract`/`format`, overrides, and validation errors at load time."
---

Each provider is one `*.toml` file under `$ATI_DIR/manifests/` (default `~/.ati/manifests/`). `ManifestRegistry::load` reads every file, parses `[provider]` plus optional `[[tools]]`, runs handler-specific validation, auto-registers OpenAPI and `file_manager` tools, and builds a flat `tool_name → (provider, tool)` index used by `ati run` and the proxy.

```mermaid
flowchart TB
  subgraph disk["$ATI_DIR"]
    M["manifests/*.toml"]
    S["specs/*.json"]
    C["cache/providers/*.json"]
  end
  subgraph load["ManifestRegistry::load"]
    P["toml::from_str → Manifest"]
    V["Handler validation"]
    O["openapi::load_and_register"]
    FM["register_file_manager_provider"]
    I["tool_index HashMap"]
  end
  M --> P --> V
  S --> O
  O --> I
  V --> I
  C --> P
  FM --> I
```

## File layout

| Path | Role |
|------|------|
| `$ATI_DIR/manifests/<name>.toml` | Permanent provider definition |
| `$ATI_DIR/specs/<spec>.json` | OpenAPI specs referenced by `openapi_spec` |
| `$ATI_DIR/cache/providers/<name>.json` | Ephemeral providers from `ati provider load` (skipped when a TOML manifest with the same `name` exists) |

One file = one `[provider]` block. Hand-written HTTP providers add one or more `[[tools]]` sections. MCP, OpenAPI, CLI (optional), passthrough, and `file_manager` providers often ship with zero `[[tools]]` and rely on discovery or built-in registration.

## Top-level shape

```toml
[provider]
name = "my_api"
description = "Human-readable summary"
handler = "http"          # default when omitted
base_url = "https://api.example.com/v1"
auth_type = "bearer"
auth_key_name = "my_api_key"

[[tools]]
name = "search"
description = "Search endpoint"
endpoint = "/search"
method = "POST"
scope = "tool:search"

[tools.input_schema]
type = "object"
required = ["query"]

[tools.input_schema.properties.query]
type = "string"

[tools.response]
extract = "$.results[*]"
format = "markdown_table"
```

<Note>
Serde accepts unknown keys on `[provider]` silently. Only fields defined on `Provider` and `Tool` in `src/core/manifest.rs` affect runtime.
</Note>

## `[provider]` — common fields

| Field | Type | Default | Purpose |
|-------|------|---------|---------|
| `name` | string | — | Provider id; used in tool prefixes and dispatch |
| `description` | string | — | Shown in `ati tool list` / assist context |
| `handler` | string | `http` | Dispatch path: `http`, `openapi`, `mcp`, `cli`, `file_manager`, `passthrough` |
| `base_url` | string | `""` | HTTP/OpenAPI base; passthrough upstream URL (required for passthrough) |
| `auth_type` | enum | `none` | `bearer`, `header`, `query`, `basic`, `oauth2`, `url`, `none` |
| `auth_key_name` | string? | — | Keyring entry for static auth |
| `auth_header_name` | string? | — | Header name when `auth_type = "header"` (default `X-Api-Key`) |
| `auth_query_name` | string? | — | Query param when `auth_type = "query"` (default `api_key`) |
| `auth_value_prefix` | string? | — | Prefix before key in header value (e.g. `Token `) |
| `extra_headers` | map | `{}` | Headers on every request; values may use `${keyring_key}` |
| `oauth2_token_url` | string? | — | Token endpoint (relative to `base_url` or absolute) |
| `auth_secret_name` | string? | — | Keyring key for OAuth2 client secret |
| `oauth2_basic_auth` | bool | `false` | Send client credentials as Basic Auth on token request |
| `auth_session_token_env` | string? | — | Env var for per-provider JWT to proxy (default `ATI_SESSION_TOKEN`) |
| `internal` | bool | `false` | Hide from public tool listing; skips auto-scope on tools |
| `category` | string? | — | Discovery grouping (e.g. `finance`) |
| `skills` | string[] | `[]` | Skill catalog URLs/names bound to this provider |

### Auth types

| `auth_type` | Runtime behavior |
|-------------|------------------|
| `bearer` | `Authorization: Bearer <keyring[key]>` |
| `header` | Custom header from `auth_header_name` + optional `auth_value_prefix` |
| `query` | Appends `auth_query_name=<key>` to query string |
| `basic` | HTTP Basic; keyring value must be `user:password` |
| `oauth2` | Client credentials flow via `oauth2_token_url` + `auth_secret_name` |
| `url` | Key interpolated into URL placeholders (e.g. MCP URL `${serpapi_api_key}`) |
| `none` | No credential injection |

### Dynamic credentials — `[provider.auth_generator]`

Optional subprocess that mints short-lived credentials at call time (proxy or local mode).

| Field | Purpose |
|-------|---------|
| `type` | `command` or `script` |
| `command` / `args` | Executable for `command` type |
| `interpreter` / `script` | Inline script for `script` type |
| `cache_ttl_secs` | Cache duration (`0` = no cache) |
| `output_format` | `text` (trimmed stdout) or `json` |
| `env` | Subprocess env; supports `${key}`, `${JWT_SUB}`, `${JWT_SCOPE}`, `${TOOL_NAME}`, `${TIMESTAMP}` |
| `inject` | For JSON output: map JSON paths → `{ type = "header"\|"env"\|"query", name = "..." }` |
| `timeout_secs` | Subprocess cap (default `30`) |

## `[provider]` — handler-specific fields

### `handler = "http"` (default)

Hand-written `[[tools]]` with `endpoint`, `method`, and JSON Schema. Parameters without `x-ati-param-location` use legacy routing: GET → query string, POST/PUT/DELETE → JSON body.

### `handler = "openapi"`

Tools come from the spec at load time; manifest `[[tools]]` are ignored.

| Field | Purpose |
|-------|---------|
| `openapi_spec` | Filename under `$ATI_DIR/specs/` or URL |
| `openapi_include_tags` / `openapi_exclude_tags` | Tag filters |
| `openapi_include_operations` / `openapi_exclude_operations` | `operationId` filters |
| `openapi_max_operations` | Cap tool count (e.g. `50` on Finnhub) |
| `openapi_overrides.<operationId>` | Per-operation metadata (see below) |

Discovered tool names are `{provider}:{operationId}` (auto-generated ids when the spec omits `operationId`). Scopes default to `tool:{prefixed_name}` unless overridden.

**OpenAPI override block** — table key is the bare `operationId`:

```toml
[provider.openapi_overrides.getPetById]
hint = "Fetch one pet by numeric ID"
description = "Get a pet (overridden)"
tags = ["lookup"]
scope = "tool:petstore:getPetById"
response_extract = "$.pet"
response_format = "json"   # markdown_table | json | raw | (else text)
```

If the spec file is missing or fails to parse, load logs a warning and continues with **zero tools** for that provider (graceful degradation).

### `handler = "mcp"`

| Field | Purpose |
|-------|---------|
| `mcp_transport` | `stdio` (default) or `http` |
| `mcp_command` / `mcp_args` | Stdio subprocess (e.g. `npx`, `["-y", "@modelcontextprotocol/server-github"]`) |
| `mcp_url` | HTTP/Streamable HTTP endpoint |
| `mcp_env` | Env for stdio; `${github_token}` resolves from keyring |
| `mcp_url_env` | **HTTP only.** Sandbox env var whose value the proxy reads as upstream URL override |

MCP tools register at runtime as `{provider}:{mcp_tool_name}` after `tools/list`. No `[[tools]]` required in the manifest.

```toml
# manifests/github-mcp.toml — stdio MCP
[provider]
name = "github"
handler = "mcp"
mcp_transport = "stdio"
mcp_command = "npx"
mcp_args = ["-y", "@modelcontextprotocol/server-github"]
auth_type = "none"

[provider.mcp_env]
GITHUB_PERSONAL_ACCESS_TOKEN = "${github_token}"
```

```toml
# manifests/deepwiki-mcp.toml — HTTP MCP
[provider]
name = "deepwiki"
handler = "mcp"
mcp_transport = "http"
mcp_url = "https://mcp.deepwiki.com/mcp"
auth_type = "none"
```

### `handler = "cli"`

| Field | Purpose |
|-------|---------|
| `cli_command` | Binary name (e.g. `gws`, `gh`) |
| `cli_default_args` | Prepended to every invocation |
| `cli_env` | `${key}` = keyring string; `@{key}` = credential file path |
| `cli_timeout_secs` | Default `120` |
| `cli_output_args` | Flags like `--output` whose value is a capture path (proxy mode) |
| `cli_output_positional` | Map subcommand prefix → 0-based index of output file arg |

If `[[tools]]` is empty, load injects one implicit tool named after the provider with scope `tool:{provider_name}`.

```toml
# manifests/google-workspace.toml
[provider]
name = "google_workspace"
handler = "cli"
cli_command = "gws"
cli_timeout_secs = 120
auth_type = "none"
skills = ["google-workspace"]

[provider.cli_env]
GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE = "@{google_workspace_credentials}"
```

### `handler = "file_manager"`

Declares upload allowlist; built-in `file_manager:download` and `file_manager:upload` attach when `[[tools]]` is empty.

```toml
[provider]
name = "file_manager"
handler = "file_manager"
upload_default_destination = "fal"

[provider.upload_destinations.fal]
kind = "fal_storage"
key_ref = "fal_api_key"

[provider.upload_destinations.gcs]
kind = "gcs"
bucket = "my-bucket"
prefix = "uploads"
```

Empty `upload_destinations` disables uploads at runtime.

### `handler = "passthrough"`

Raw HTTP reverse-proxy routes on `ati proxy` — typically **no** `[[tools]]`.

| Field | Default | Purpose |
|-------|---------|---------|
| `host_match` | — | Route by `Host` header |
| `path_prefix` | — | Route by URI prefix (must start with `/`; trailing `/` normalized) |
| `strip_prefix` | `true` | Remove prefix before forwarding |
| `path_replace` | — | Tuple `["/from", "/to"]` after strip |
| `host_override` | — | Upstream `Host` header only (SNI follows `base_url`) |
| `deny_paths` | `[]` | Post-rewrite path globs → 403 |
| `forward_authorization_paths` | `[]` | Forward sandbox `Authorization` instead of manifest auth |
| `connect_timeout_seconds` | `5` | TCP connect |
| `read_timeout_seconds` | `300` | Full response read |
| `idle_timeout_seconds` | `60` | Pool idle |
| `max_request_bytes` / `max_response_bytes` | 100MB / 500MB | `0` = unlimited |
| `forward_websockets` | `false` | Requires `http://` or `https://` `base_url` when enabled |

At least one of `host_match` or `path_prefix` is required; `base_url` must be non-empty.

## `[[tools]]` — per-tool fields

| Field | Default | Purpose |
|-------|---------|---------|
| `name` | — | Registry key (see naming below) |
| `description` | — | Tool docs / assist |
| `endpoint` | `""` | Path appended to `base_url` (HTTP/OpenAPI) |
| `method` | `GET` | `GET`, `POST`, `PUT`, `DELETE` (case-insensitive aliases accepted) |
| `scope` | auto | JWT scope gate; see below |
| `input_schema` | — | JSON Schema object for CLI args |
| `response` | passthrough | Post-processing (see next section) |
| `tags` | `[]` | Discovery tags |
| `hint` | — | Short LLM guidance |
| `examples` | `[]` | Example invocations |

### Tool naming conventions

| Handler | Indexed `name` | Example |
|---------|----------------|---------|
| HTTP (hand-written) | As written in manifest | `hackernews_top_stories` |
| OpenAPI | `{provider}:{operationId}` | `finnhub:quote` |
| MCP (discovered) | `{provider}:{mcp_name}` | `github:search_repositories` |
| CLI (auto) | `{provider}` | `google_workspace` |
| `file_manager` (built-in) | `file_manager:download`, `file_manager:upload` | Fixed |

Duplicate `name` values across files: **last loaded file wins** in the `tool_index` map.

### Scope auto-assignment

After load, any tool without `scope` on a non-`internal` provider gets `scope = "tool:{tool.name}"`. Internal providers (e.g. `_llm` for `ati assist`) leave `scope` unset so tools stay outside default JWT filtering.

Shared scopes are valid — multiple Hacker News tools use `scope = "tool:hackernews_stories"` for one JWT grant.

## `[tools.response]` — extract and format

Processed in `core/response.rs` after the upstream returns JSON.

| Field | Values | Behavior |
|-------|--------|----------|
| `extract` | JSONPath string | Subset of body; empty match → `null`; multiple matches → array |
| `format` | `text` (default), `json`, `markdown_table`, `raw` | CLI/table rendering hint |

Omit `[tools.response]` to return the raw parsed body. Invalid JSONPath fails the call with `JSONPath extraction failed`.

```toml
[tools.response]
extract = "$.results[*]"
format = "markdown_table"
```

OpenAPI overrides use `response_extract` / `response_format` on the override table instead of a nested `[tools.response]` block.

## Load-time validation and errors

`ManifestError` variants:

| Variant | When |
|---------|------|
| `NoDirectory` | Manifest path is not a directory |
| `Io` | Read/glob failure |
| `Parse` | Invalid TOML (`toml::de::Error`) |
| `Invalid` | Semantic validation failed |

### `Invalid` messages (fail load for that file)

| Condition | Message pattern |
|-----------|-----------------|
| `file_manager` + `upload_default_destination` not in map | `upload_default_destination 'X' is not present in [provider.upload_destinations]` |
| `mcp_url_env` empty/whitespace | `mcp_url_env must not be empty when set` |
| `mcp_url_env` bad POSIX name | `mcp_url_env '…' is not a valid POSIX env var name` |
| `mcp_url_env` on non-HTTP MCP | `mcp_url_env requires handler = "mcp" and mcp_transport = "http"` |
| Passthrough empty `base_url` | `passthrough provider requires non-empty base_url` |
| Passthrough no route | `passthrough provider requires at least one of host_match or path_prefix` |
| `path_prefix` without leading `/` | `passthrough path_prefix must start with '/'` |
| Empty `host_match` | `passthrough host_match must be non-empty when set` |
| `forward_websockets` + bad scheme | `forward_websockets requires base_url to use http:// or https://` |
| `path_replace` source without `/` | `passthrough path_replace source must start with '/'` |

OpenAPI spec errors do **not** produce `Invalid`; they warn and register the provider with no tools.

After all TOML files load, `register_file_manager_provider` ensures `file_manager` exists (default empty destinations if no operator manifest).

## Minimal examples by handler

<CodeGroup>
```toml title="HTTP — manifests/hackernews.toml"
[provider]
name = "hackernews"
base_url = "https://hacker-news.firebaseio.com/v0"
auth_type = "none"

[[tools]]
name = "hackernews_top_stories"
endpoint = "/topstories.json"
method = "GET"
scope = "tool:hackernews_stories"

[tools.response]
format = "json"
```

```toml title="OpenAPI — manifests/finnhub.toml"
[provider]
name = "finnhub"
handler = "openapi"
base_url = "https://finnhub.io/api/v1"
openapi_spec = "finnhub.json"
auth_type = "query"
auth_query_name = "token"
auth_key_name = "finnhub_api_key"
openapi_max_operations = 50
```

```toml title="Internal — manifests/_llm.toml"
[provider]
name = "_llm"
internal = true
auth_type = "bearer"
auth_key_name = "cerebras_api_key"

[[tools]]
name = "_chat_completion"
endpoint = "/chat/completions"
method = "POST"
```
</CodeGroup>

## Verification

<Steps>
<Step title="Place manifest">
Copy `my_provider.toml` into `$ATI_DIR/manifests/`. For OpenAPI, place the spec in `$ATI_DIR/specs/`.
</Step>
<Step title="List tools">
Run `ati tool list` and confirm provider tools appear (MCP tools appear after first discovery call or proxy startup).
</Step>
<Step title="Inspect one tool">
Run `ati tool info <name>` — for OpenAPI/MCP use the prefixed name (`finnhub:quote`, `github:search_repositories`).
</Step>
</Steps>

<Warning>
A silent OpenAPI load failure looks like an empty provider in `ati tool list`. Check logs with `RUST_LOG=warn` or verify the spec path under `$ATI_DIR/specs/`.
</Warning>

## Related pages

<CardGroup>
<Card title="Providers and handlers" href="/providers-and-handlers">
How each `handler` maps to `core/http`, `mcp_client`, and CLI execution.
</Card>
<Card title="CLI and HTTP manifests" href="/cli-and-http-manifests">
Hand-written tools, `${key}` / `@{key}`, and `cli_output_args`.
</Card>
<Card title="Import OpenAPI" href="/import-openapi">
Spec import, filters, and `x-ati-param-location`.
</Card>
<Card title="Add MCP providers" href="/add-mcp-providers">
`ati provider add-mcp` and namespaced MCP tool calls.
</Card>
<Card title="JWT and scopes reference" href="/jwt-scopes-reference">
Scope grammar matched against `tools.scope` and auto-assigned scopes.
</Card>
</CardGroup>

---

## 19. JWT and scopes reference

> Claims (`sub`, `aud`, `scope`, `exp`, `jti`), ES256 vs HS256, scope grammar (`tool:`, `skill:`, `help`, `*`), discovery filtering, and validation failure modes.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/19-jwt-and-scopes-reference.md
- Generated: 2026-06-02T03:18:42.758Z

### Source Files

- `src/core/jwt.rs`
- `src/core/scope.rs`
- `docs/SECURITY.md`
- `docs/JWT_STANDARDS_2026.md`
- `src/cli/token.rs`
- `ati-client/python/src/ati/scope.py`

---
title: "JWT and scopes reference"
description: "Claims (`sub`, `aud`, `scope`, `exp`, `jti`), ES256 vs HS256, scope grammar (`tool:`, `skill:`, `help`, `*`), discovery filtering, and validation failure modes."
---

ATI carries authorization in a single JWT: the orchestrator signs it, sandboxes pass it as `ATI_SESSION_TOKEN`, and the proxy (or local CLI when JWT keys are configured) validates signature, audience, and expiry before applying the space-delimited `scope` claim to tool calls, discovery endpoints, and skill visibility.

## Runtime model

```mermaid
sequenceDiagram
    participant Orch as Orchestrator
    participant ATI as ati CLI / proxy
    participant Up as Upstream API

    Orch->>Orch: Sign TokenClaims (ES256 or HS256)
    Orch->>ATI: ATI_SESSION_TOKEN (+ optional _FILE)
    ATI->>ATI: jwt::validate (sig, aud, exp, iss?)
    ATI->>ATI: ScopeConfig::from_jwt
    alt /call or ati run
        ATI->>ATI: check_access(tool_name, tool.scope)
        ATI->>Up: Execute with injected credentials
    else Discovery (/tools, /skills, assist)
        ATI->>ATI: filter_tools_by_scope / visible_skills
    end
```

| Mode | JWT required? | Scope behavior |
|------|---------------|----------------|
| Proxy with `ATI_JWT_PUBLIC_KEY` or `ATI_JWT_SECRET` | Yes — `Authorization: Bearer` on named routes | Strict; missing/invalid token → HTTP 401 |
| Proxy without JWT keys | No | `ScopeConfig::unrestricted()` |
| Local CLI with JWT env configured | Yes — `ATI_SESSION_TOKEN` | Validates token; enforces per-tool `scope` on `ati run` |
| Local CLI without JWT env | No | Unrestricted dev mode |

<Note>
`Ati-Key` authentication (database virtual keys, `db` feature) synthesizes `TokenClaims` from stored scopes and bypasses JWT validation when that header is present. See [Configure JWT and keys](/configure-jwt-and-keys).
</Note>

## Standard claims

`TokenClaims` in `core/jwt.rs` follows RFC 9068-style access-token shape: space-delimited `scope`, required `sub`/`aud`/`exp`/`iat` for validation paths, and an optional `ati` namespace for ATI-specific metadata.

| Claim | Required at issue | Validated on `jwt::validate` | Role |
|-------|-------------------|------------------------------|------|
| `sub` | Yes | Yes (spec claim) | Agent or sandbox identity; copied into `ScopeConfig.sub` |
| `aud` | Yes (default `ati-proxy`) | Yes — must match **any** entry in `JwtConfig.accepted_audiences` | Prevents token substitution across services |
| `exp` | Yes (`iat + ttl`) | Yes | Scope expiry; `ScopeConfig` also re-checks `exp` on access |
| `iat` | Yes | Decoded | Issued-at timestamp (Unix seconds) |
| `scope` | Yes (may be empty string) | Not structurally validated | Space-delimited allowlist; parsed via `TokenClaims::scopes()` |
| `iss` | Optional | Yes when `ATI_JWT_ISSUER` / `required_issuer` set | Issuer allowlist |
| `jti` | Auto-generated on `ati token issue` | Stored, not replay-checked yet | Unique token id for future revocation |
| `job_id` | Optional | No extra check | Orchestrator job correlation |
| `sandbox_id` | Optional | No extra check | Sandbox correlation |
| `ati` | Default namespace on issue | Preserved round-trip | See [ATI namespace](#ati-namespace) |

### ATI namespace

```json
{
  "ati": {
    "v": 1,
    "rate": { "tool:github:*": "10/hour" },
    "customer_id": "cust_alpha"
  }
}
```

| Field | Purpose |
|-------|---------|
| `v` | Claims schema version (currently `1`) |
| `rate` | Per-pattern rate limits; parsed into `ScopeConfig.rate_config` and enforced on `/call` and local `ati run` |
| `customer_id` | Tenant id for proxy credential cascade (customer row → shared row); older tokens without the field deserialize as `None` |

## Scope claim grammar

The `scope` claim is a **single string** of space-separated tokens (RFC 9068 §2.2.3), not a JSON array.

| Pattern | Example | Matches |
|---------|---------|---------|
| Global wildcard | `*` | Any tool scope, `help`, and skill checks via `matches_wildcard` |
| Exact tool | `tool:web_search` | Tool whose manifest `scope` equals `tool:web_search` |
| Provider wildcard | `tool:github:*` | Any tool scope starting with `tool:github:` |
| Skill prefix | `skill:research-*` | Skill name prefix after `skill:` |
| Explicit skill | `skill:my-skill` | Remote/local skill when name is in catalog/registry |
| Help | `help` | Sets `ScopeConfig::help_enabled()` (status reporting; include in tokens that need assist) |
| Legacy underscore | `tool:github_search_repos` | Canonical `tool:github:search_repos` via alias normalization |

### Tool scope strings on manifests

Each public tool gets a `scope` field used at enforcement time:

- Hand-written manifests: set `[[tools]].scope` explicitly, or rely on auto-assignment.
- Auto-assignment: if `scope` is unset and the provider is not `internal`, load time sets `tool:{tool.name}` (full registered name, including `provider:tool` for MCP/OpenAPI).
- OpenAPI import: default `tool:{prefixed_operation_name}` unless overridden.

Authorization compares the JWT scope list against the tool’s `scope` string using `ScopeConfig::is_allowed`, not the CLI tool name alone.

### Wildcard matching rules

`matches_wildcard(name, pattern)` in `core/scope.rs`:

1. `pattern == "*"` → allow.
2. Exact string equality → allow.
3. `pattern` ends with `*` → allow if `name.starts_with(prefix)` where `prefix` is everything before `*`.

There is no infix `*`; `tool:github:*` does not match `tool:linear:list_issues`.

### Building scope strings (Python client)

```python
from ati import build_scope_string

build_scope_string(
    tools=["web_search", "github:*"],
    skills=["research-*"],
    extra=["help"],
)
# → "tool:web_search tool:github:* skill:research-* help"
```

Rust equivalent at issue time:

```bash
ati token issue \
  --sub agent-7 \
  --scope "tool:web_search tool:github:* skill:research-* help" \
  --ttl 1800
```

## ES256 vs HS256

| Aspect | ES256 (recommended) | HS256 |
|--------|---------------------|-------|
| Key material | `ATI_JWT_PUBLIC_KEY` + optional `ATI_JWT_PRIVATE_KEY` PEM | `ATI_JWT_SECRET` (hex-encoded shared secret) |
| Who can issue | Holder of private key only | Any party that knows the secret |
| Who can validate | Anyone with public key / JWKS | Anyone with the same secret |
| JWKS endpoint | Yes — `GET /.well-known/jwks.json` when public PEM configured | No — `public_key_pem` is `None` |
| Default leeway | 60 seconds | 60 seconds |
| `ati token keygen` | `--algorithm ES256` (default) | `--algorithm HS256` |

`jwt::config_from_env()` resolution order:

1. `ATI_JWT_PUBLIC_KEY` → ES256 `JwtConfig` (private key optional; validation-only proxies omit it).
2. Else `ATI_JWT_SECRET` → HS256 `config_from_secret`.
3. Else `None` → JWT disabled (dev/unrestricted paths).

<Warning>
HS256 is appropriate for single-machine or tightly coupled issuer/validator pairs. For multi-tenant proxy deployments, prefer ES256 so sandboxes and validators never hold signing material.
</Warning>

### Audience allowlist

`aud` on the token is a single string. The validator accepts it if it equals **any** value in `accepted_audiences`:

| Env var | Behavior |
|---------|----------|
| `ATI_JWT_ACCEPTED_AUDIENCES` | CSV allowlist (first non-empty wins), e.g. `ati-proxy,parcha-tools` |
| `ATI_JWT_AUDIENCE` | Back-compat single audience (wrapped in one-element vec) |
| (unset) | Default `["ati-proxy"]` |

`validate()` hard-errors if `accepted_audiences` is empty (prevents jsonwebtoken’s silent “accept any aud” behavior).

## Session token delivery

Sandboxes receive the raw JWT via `ATI_SESSION_TOKEN`. Resolution order (`core/token.rs`):

1. Non-empty `ATI_SESSION_TOKEN` env var.
2. `ATI_SESSION_TOKEN_FILE` path, else `/run/ati/session_token`.
3. Per-provider env vars (e.g. `PARCHA_TOOLS_SESSION_TOKEN`) use the same pattern with slugified default paths under `/run/ati/`.

Each `ati` invocation re-reads the file (no in-process cache) so supervisors can rotate tokens without restarting agents.

## Scope enforcement surfaces

After JWT validation, `ScopeConfig::from_jwt` drives authorization.

### Tool execution

| Surface | Denied signal |
|---------|---------------|
| Proxy `POST /call` | HTTP 403, `"Access denied: '{tool}' is not in your scopes"` |
| Proxy, JWT on but no Bearer | HTTP 403, `"Authentication required — no JWT provided"` |
| Local `ati run` | `ScopeError::AccessDenied` / `Expired` printed to stderr |
| Expired scopes | Denied even if signature still parses within leeway window — `is_expired()` uses wall clock vs `exp` |

Tools with **empty** `scope` are always allowed once authenticated.

### Discovery filtering

Scope-filtered listing prevents enumeration of tools and skills outside the token:

| Endpoint / command | Filter mechanism |
|--------------------|------------------|
| `GET /tools`, `ati tool list` | `filter_tools_by_scope` on public tools |
| MCP `tools/list` via `POST /mcp` | Same visible subset |
| `GET /skills`, `ati skill list` | `visible_skills` / `visible_skill_names` |
| `POST /help`, `ati assist` | LLM context built only from `visible_tools_for_scopes` + resolved skills |
| `GET /skillati/*` | `visible_skill_names_with_remote` (explicit `skill:X` + tool/provider/category cascade) |
| `POST /skills/resolve` | Transitive resolution within visible set |

Wildcard JWT scope `*` skips tool filtering and exposes the full public catalog (skills still follow resolver rules unless `*` short-circuits remote catalog).

### Skill resolution cascade

Beyond explicit `skill:name` scopes, skills become visible when their bindings intersect allowed tools:

1. `skill:X` in JWT → skill X if present.
2. `tool:Y` (including wildcards) → skills listing tool Y.
3. Tool’s provider → skills bound to that provider.
4. Provider category → skills bound to that category.
5. `depends_on` → transitive loads during `resolve_skills` (for assist/resolve paths).

A token with only `help` does not grant skill visibility by itself (integration tests treat `help` alone as skill-denied).

### Help scope

`ScopeConfig::help_enabled()` is true when scopes contain `help` or `*`. `ati auth status` reports this flag. Operational docs recommend adding `help` to tokens that use `ati assist` or `POST /help`; discovery still requires tool/skill scopes to populate meaningful context.

## JWT validation failure modes

### Cryptographic / structural (`jwt::validate`)

| Failure | Typical cause | Proxy HTTP | Local CLI |
|---------|---------------|------------|-------------|
| Invalid signature | Wrong secret/key, tampered payload | 401 Unauthorized | `Invalid ATI_SESSION_TOKEN: ...` |
| Expired `exp` | TTL elapsed beyond leeway (60s) | 401 | Same |
| Wrong `aud` | Token minted for different service | 401 | Same |
| Wrong `iss` | `ATI_JWT_ISSUER` mismatch | 401 | Same |
| Malformed JWT | Not three base64url segments | 401 | Same |
| Empty audience config | Misconfigured `accepted_audiences` | 401 (`InvalidKey` message) | Same |

`ati token inspect` decodes payload **without** signature verification (debug only).

`ati token validate` mirrors proxy rules: uses `ATI_JWT_ACCEPTED_AUDIENCES` / `ATI_JWT_AUDIENCE` for audience checks.

### Scope errors (post-validation)

| Error | When | User-visible message |
|-------|------|----------------------|
| `ScopeError::AccessDenied(tool_name)` | Tool scope not matched | `Access denied: '{tool_name}' is not in your scopes` |
| `ScopeError::Expired(ts)` | `now > expires_at` from JWT `exp` | `Scopes have expired (expired at {ts})` |
| Rate limit | `ati.rate` pattern match | HTTP 429 on proxy; error string locally |

### Dev vs production misconfiguration

| Symptom | Likely cause |
|---------|--------------|
| All tools visible locally | No `ATI_JWT_PUBLIC_KEY` / `ATI_JWT_SECRET` — unrestricted dev mode |
| `ATI_SESSION_TOKEN is required because JWT validation is configured locally` | Keys set but token missing |
| Proxy returns 401 on every route | JWT configured but missing/invalid Bearer |
| Proxy returns 403 on `/call` with valid JWT | Scope string lacks required `tool:...` pattern |
| `ati auth status` shows `Verified: NO` | Inspect works; full validate needs matching public key/secret in env |

## CLI commands

<Steps>
<Step title="Generate keys">
<CodeGroup>
```bash title="ES256 (production)"
ati token keygen --algorithm ES256
# Set ATI_JWT_PRIVATE_KEY and ATI_JWT_PUBLIC_KEY to PEM paths
```

```bash title="HS256 (simple)"
ati token keygen --algorithm HS256
# Set ATI_JWT_SECRET to printed hex
```
</CodeGroup>
</Step>
<Step title="Issue a scoped token">
```bash
ati token issue \
  --sub sandbox-abc \
  --scope "tool:finnhub:* tool:github:* help" \
  --ttl 1800 \
  --aud ati-proxy \
  --rate "tool:github:*=10/hour"
export ATI_SESSION_TOKEN="<printed token>"
```
Default TTL is **1800** seconds (30 minutes).
</Step>
<Step title="Verify before deploy">
```bash
ati token inspect "$ATI_SESSION_TOKEN"
ati token validate "$ATI_SESSION_TOKEN"
ati auth status
```
</Step>
</Steps>

| Command | Purpose |
|---------|---------|
| `ati token keygen` | Create ES256 PEM pair or HS256 hex secret |
| `ati token issue` | Sign `TokenClaims`; auto `jti` UUID; default `ati` namespace |
| `ati token inspect` | JSON dump of claims (no signature check) |
| `ati token validate` | Full verify; exits 1 on failure |
| `ati auth status` | Human/json summary: scopes, expiry, `help_enabled`, signature verified |

## Environment variables (JWT)

| Variable | Role |
|----------|------|
| `ATI_JWT_PUBLIC_KEY` | ES256 public PEM path (validation + JWKS) |
| `ATI_JWT_PRIVATE_KEY` | ES256 private PEM path (issuance) |
| `ATI_JWT_SECRET` | HS256 hex secret (issue + validate) |
| `ATI_JWT_ISSUER` | Required issuer when set |
| `ATI_JWT_AUDIENCE` | Single accepted audience (default `ati-proxy`) |
| `ATI_JWT_ACCEPTED_AUDIENCES` | CSV multi-audience allowlist |
| `ATI_SESSION_TOKEN` | Bearer token for CLI and proxy client |
| `ATI_SESSION_TOKEN_FILE` | Hot-rotation file path override |

## Minimal token examples

<RequestExample>
```json
{
  "sub": "agent-7",
  "aud": "ati-proxy",
  "iat": 1748736000,
  "exp": 1748737800,
  "jti": "dbe39bf3-a3ba-4238-a513-f51d6e1691c4",
  "scope": "tool:web_search tool:github:* skill:research-* help",
  "iss": "ati-orchestrator",
  "ati": { "v": 1, "customer_id": "cust_alpha" }
}
```
</RequestExample>

<Tip>
Use `ati token issue` for production tokens so `jti`, `iat`, and `exp` stay consistent with proxy validation defaults.
</Tip>

## Related pages

<CardGroup>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
Key generation, `ati init --proxy`, orchestrator issuance patterns, and session token files.
</Card>
<Card title="Scopes and tool discovery" href="/scopes-and-tool-discovery">
Discovery tiers (`ati tool search`, `ati tool info`, `ati assist`) on top of scope-filtered catalogs.
</Card>
<Card title="Execution modes" href="/execution-modes">
Local keyring vs proxy mode and where credentials vs JWT scopes live.
</Card>
<Card title="Proxy API reference" href="/proxy-api-reference">
`/call`, `/help`, `/tools`, `/skillati/*`, auth headers, and response shapes.
</Card>
<Card title="Environment variables" href="/environment-variables">
Full runtime env contract including JWT and session token variables.
</Card>
<Card title="Security and production" href="/security-and-production">
Threat model, scope enumeration prevention, and proxy hardening checklist.
</Card>
</CardGroup>

---

## 20. Assist and plan reference

> `ati assist` flags (`--plan`, `--save`, `--local`), internal `_llm` provider, structured plan output, and `ati plan` execution of saved tool-call plans.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/20-assist-and-plan-reference.md
- Generated: 2026-06-02T03:19:32.124Z

### Source Files

- `src/cli/help.rs`
- `src/cli/plan.rs`
- `docs/assist-examples.md`
- `manifests/_llm.toml`
- `tests/plan_test.rs`
- `src/core/skill.rs`

---
title: "Assist and plan reference"
description: "`ati assist` flags (`--plan`, `--save`, `--local`), internal `_llm` provider, structured plan output, and `ati plan` execution of saved tool-call plans."
---

`ati assist` is ATI’s LLM-backed discovery command: it assembles scope-filtered tool and skill context, calls the internal `_llm` provider (or Anthropic / local OpenAI-compatible fallbacks), and returns either conversational `ati run` guidance or a machine-readable plan. `ati plan execute` replays saved plan JSON by dispatching each step through the same `ati run` stack used for single calls.

## Command surface

| Command | Purpose |
|---------|---------|
| `ati assist [scope] <query…>` | Natural-language tool discovery and usage guidance |
| `ati assist --plan [scope] <query…>` | Same context, but LLM must return structured JSON steps |
| `ati assist --save <path> [scope] <query…>` | Implies `--plan`; writes pretty-printed plan JSON to `<path>` |
| `ati plan execute <file>` | Validate tools, then run each plan step via `ati run` |
| `ati plan execute <file> --confirm-each` | Prompt before each step on an interactive TTY |

Positional arguments use `trailing_var_arg`: when the first token matches a registered tool or provider name, assist runs in **scoped** mode; otherwise the full argument list is the query.

<ParamField body="--plan" type="flag">
Return a structured plan of tool calls instead of prose. Always uses the local assist pipeline (does not forward to the proxy).
</ParamField>

<ParamField body="--save" type="string">
Output path for plan JSON. Setting this flag enables plan mode even without `--plan`.
</ParamField>

<ParamField body="--local" type="flag">
Force the OpenAI-compatible local LLM path (`OLLAMA_HOST`, default `http://localhost:11434`). Also honored when `ATI_ASSIST_PROVIDER=local`.
</ParamField>

## Runtime routing

```mermaid
flowchart TB
  subgraph assist_cli["ati assist"]
    A[Parse args + scope]
    B{--plan or --save?}
    C[execute_plan_mode\nlocal LLM only]
    D{ATI_PROXY_URL set?}
    E[POST proxy /help]
    F[execute_local\nbuild context + call_llm]
  end
  subgraph llm_backends["LLM backends"]
    G[Cerebras via _llm.toml]
    H[Anthropic Messages API]
    I[Local OpenAI-compatible]
  end
  subgraph replay["ati plan execute"]
    J[Load plan JSON]
    K[Validate tools in registry]
    L[ati run per step]
  end
  A --> B
  B -->|yes| C --> G
  B -->|no| D
  D -->|yes| E
  D -->|no| F
  F --> G
  F --> H
  F --> I
  C --> G
  J --> K --> L
```

<Note>
Plan mode (`--plan` / `--save`) never uses `ATI_PROXY_URL`. Even in proxy deployments, plan generation runs locally with manifests, keyring, and MCP discovery from `~/.ati/`.
</Note>

Prose assist without plan flags:

- **`ATI_PROXY_URL` set** — forwards to `POST /help` on the proxy, JWT via `ATI_SESSION_TOKEN`.
- **Otherwise** — loads `~/.ati/manifests`, discovers MCP tools, filters by JWT scopes when validation is configured, and calls `call_llm` directly.

## Internal `_llm` provider

The bundled manifest `manifests/_llm.toml` defines an **internal** provider (hidden from `ati tool list`) used only for assist:

| Field | Value |
|-------|-------|
| Provider | `_llm` |
| Tool | `_chat_completion` |
| Endpoint | `POST /chat/completions` on `https://api.cerebras.ai/v1` |
| Model | `zai-glm-4.7` |
| Auth key | `cerebras_api_key` in keyring (or `CEREBRAS_API_KEY` env, checked first) |
| Generation | `max_completion_tokens: 1536`, `temperature: 0.3` |

Proxy `POST /help` uses the same manifest and keyring entry. If `_llm.toml` or the key is missing, the proxy returns **503** with an explicit error. Internal tools never appear in the assist system prompt’s tool list (`list_public_tools` excludes `internal = true` providers).

## LLM selection order

`call_llm` resolves backends in this order (unless `--local` / `ATI_ASSIST_PROVIDER=local` forces local first):

<Steps>
<Step title="Local override">
`--local` or `ATI_ASSIST_PROVIDER=local` → `call_local_llm` against `{OLLAMA_HOST}/v1/chat/completions` (120s timeout).
</Step>
<Step title="Cerebras">
`CEREBRAS_API_KEY` env, else keyring `cerebras_api_key` via `_chat_completion` manifest.
</Step>
<Step title="Anthropic">
`ANTHROPIC_API_KEY` → `https://api.anthropic.com/v1/messages`; model from `ATI_ASSIST_MODEL` (default `claude-haiku-4-5-20251001`).
</Step>
<Step title="Local fallback">
If no cloud keys are set, auto-attempt local LLM; failure surfaces install hints for Ollama and `OLLAMA_HOST`.
</Step>
</Steps>

| Variable | Default | Role |
|----------|---------|------|
| `CEREBRAS_API_KEY` | — | Preferred cloud key (checked before keyring) |
| `ANTHROPIC_API_KEY` | — | Anthropic fallback |
| `ATI_ASSIST_MODEL` | `claude-haiku-4-5-20251001` | Anthropic model id |
| `ATI_ASSIST_PROVIDER` | auto | Set to `local` to force local LLM |
| `OLLAMA_HOST` | `http://localhost:11434` | Local server base URL |
| `ATI_OLLAMA_MODEL` | `smollm3:3b` | Local model name |

<Warning>
Local models on CPU are slow and may ignore large tool contexts. Production assist should use Cerebras or Anthropic; `--local` is intended for air-gapped or sandboxed environments.
</Warning>

## Context assembly

### Tool catalog

- Unscoped queries pre-filter up to **50** tools with the same fuzzy scorer as `ati tool search` (`prefilter_tools_by_query`).
- Scoped queries load one tool’s schema, or all tools for a provider name.
- CLI providers optionally embed live `--help` output (5s timeout, max 3000 chars per capture; skipped when more than five CLI tools unless scoped).
- JWT scopes from `load_local_scopes_from_env` filter tools before prompting; without JWT config, local mode is unrestricted for development.

### Skills

Matched skills inject SKILL.md content (up to **16 000** chars total, **4 000** per skill) via `build_skills_for_tools`:

1. Skills bound to matched tool names  
2. Skills bound to those tools’ providers  
3. Keyword search on tool-name tokens  

Proxy `/help` additionally merges remote SkillATI catalog sections. See [Skills and SkillATI](/skills-and-skillati).

### Prose output extras

Text mode appends a **Quick Reference** block when the model mentions tool names from context: auto-generated `ati run` usage cards and parameter summaries from each tool’s JSON Schema. JSON mode (`--output json` / `-J`) returns:

```json
{
  "content": "<llm text>",
  "tools_referenced": ["tool_a", "tool_b"]
}
```

Proxy JSON assist sets `tools_referenced` to an empty array (the proxy does not post-process mentions).

## Plan mode

Plan mode reuses the same context builder as prose assist, then appends a plan-mode system prompt suffix instructing the model to reply with **only** JSON:

```json
{
  "steps": [
    {
      "tool": "<tool_name>",
      "args": { "<key>": "<value>" },
      "description": "<what this step does>"
    }
  ]
}
```

`parse_plan_response` accepts raw JSON or JSON inside markdown fences (`` ```json `` / `` ``` ``). It builds a `Plan` object:

<ResponseField name="query" type="string">
Original user query (from resolved assist args).
</ResponseField>

<ResponseField name="steps" type="array">
Ordered `PlanStep` entries; missing `args` defaults to `{}`.
</ResponseField>

<ResponseField name="created_at" type="string">
UTC timestamp in RFC3339 from `chrono::Utc::now()`.
</ResponseField>

Each `PlanStep` contains `tool`, `args` (`HashMap<String, Value>`), and `description`.

<RequestExample>

```bash
ati assist --plan "get top HN stories then search the web for the top headline"
```

</RequestExample>

<RequestExample>

```bash
ati assist --save ./workflow.json finnhub "quote and insider activity for AAPL"
```

</RequestExample>

On parse failure, the CLI exits with `Failed to parse plan from LLM response` and prints the raw model output for debugging.

## `ati plan execute`

<Steps>
<Step title="Load and parse">
Read the plan file as JSON into `Plan`.
</Step>
<Step title="Validate">
Ensure every `step.tool` exists in `ManifestRegistry` before any execution (`unknown tool` error names the step index).
</Step>
<Step title="Run steps">
For each step, convert `args` to `--key value` pairs (non-strings use `Value::to_string()`), then call `call::execute` (respects `ATI_PROXY_URL` like a normal `ati run`).
</Step>
</Steps>

| Behavior | Detail |
|----------|--------|
| Success | Prints `[OK]` on stderr |
| Failure | Prints `[ERROR] <message>`; with `--confirm-each` on a TTY, offers to continue |
| Skip | `--confirm-each` + `n` skips the step |
| Abort | `--confirm-each` + error + `n` stops with `Plan execution aborted at step N` |

Example plan file:

```json
{
  "query": "find info",
  "steps": [
    {
      "tool": "hackernews:top_stories",
      "args": { "limit": 5 },
      "description": "Get top stories"
    },
    {
      "tool": "web_search",
      "args": { "query": "rust async" },
      "description": "Search web"
    }
  ],
  "created_at": "2025-01-01T00:00:00Z"
}
```

```bash
ati plan execute ./workflow.json
ati plan execute ./workflow.json --confirm-each
```

## Proxy `POST /help`

:::endpoint POST /help
LLM-powered assist for sandboxed agents. Requires JWT when proxy JWT validation is enabled. Body: `{ "query": string, "tool"?: string }`. Response: `{ "content": string, "error"?: string }`.
:::

The handler mirrors local assist scoping, uses scope-filtered `list_public_tools`, resolves skills (local + remote SkillATI), and calls Cerebras through `_chat_completion`. Documented security policy: include the `help` scope (or `*`) in session tokens so agents may use discovery endpoints; tool visibility still follows `tool:` / `skill:` scopes ([JWT and scopes reference](/jwt-scopes-reference)).

## Scopes and JWT

When `ATI_JWT_*` validation is configured locally, `ati assist` and plan mode require a valid `ATI_SESSION_TOKEN` and only expose tools allowed by the token’s `scope` claim. `ati auth show` reports `help_enabled` when the token includes `help` or `*`. Scoped assist to a tool outside the token returns: `'name' is not visible in your current scopes.`

## Failure modes

| Symptom | Likely cause |
|---------|----------------|
| `No _llm.toml manifest found` | Missing `manifests/_llm.toml` in `ATI_DIR` |
| `LLM API key not found in keyring` | `ati key set cerebras_api_key …` not run (proxy) |
| `Failed to parse plan from LLM response` | Model returned prose or malformed JSON; retry or tighten query |
| `Step N: unknown tool` | Plan references a tool not in manifests (typo or unloaded provider) |
| `Cannot read plan file` / `Invalid plan JSON` | Bad path or corrupt plan file |
| `Scope 'X' is not visible` | JWT or scope filter excludes that tool/provider |
| Empty / useless local answers | Model too small or CPU-bound; use cloud keys |

## Related pages

<CardGroup>
<Card title="Scopes and tool discovery" href="/scopes-and-tool-discovery">
JWT scope filtering and the three discovery tiers: `ati tool search`, `ati tool info`, and `ati assist`.
</Card>
<Card title="CLI reference" href="/cli-reference">
Top-level `assist` and `plan` subcommands, global `--output` / `--verbose` flags.
</Card>
<Card title="Proxy API reference" href="/proxy-api-reference">
`POST /help` request shape, auth, and error codes.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
Issuing tokens with `help` and tool scopes for assist in sandboxes.
</Card>
<Card title="Environment variables" href="/environment-variables">
`ATI_PROXY_URL`, `ATI_SESSION_TOKEN`, `OLLAMA_HOST`, and assist-related overrides.
</Card>
<Card title="Execution modes" href="/execution-modes">
Local keyring vs proxy routing for `ati run` (including plan replay).
</Card>
<Card title="OpenAPI stock research workflow" href="/openapi-stock-research">
End-to-end Finnhub recipe using `ati assist` plus chained `ati run` calls.
</Card>
</CardGroup>

---

## 21. OpenAPI stock research workflow

> End-to-end Finnhub recipe: `import-openapi`, `ati key set`, `ati assist`, and chained `ati run` calls for quotes, insiders, and sentiment with expected JSON fields.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/21-openapi-stock-research-workflow.md
- Generated: 2026-06-02T03:20:11.148Z

### Source Files

- `README.md`
- `manifests/finnhub.toml`
- `specs/finnhub.json`
- `src/cli/call.rs`
- `src/cli/help.rs`
- `docs/assist-examples.md`

---
title: "OpenAPI stock research workflow"
description: "End-to-end Finnhub recipe: `import-openapi`, `ati key set`, `ati assist`, and chained `ati run` calls for quotes, insiders, and sentiment with expected JSON fields."
---

The Finnhub provider is an OpenAPI-backed HTTP handler (`handler = "openapi"`) that loads `specs/finnhub.json`, registers tools as `finnhub:<operationId>`, injects the API key as query parameter `token`, and executes GET requests through `ati run` with optional scope-aware discovery via `ati assist`.

## What this workflow covers

| Step | Command / surface | Outcome |
|------|-------------------|---------|
| Import | `ati provider import-openapi` | Writes `~/.ati/specs/finnhub.json` and `~/.ati/manifests/finnhub.toml` |
| Credential | `ati key set finnhub_api_key` | AES-256-GCM keyring entry used as `?token=` on every call |
| Discovery | `ati assist finnhub "…"` | Scoped LLM answer with ordered `ati run` commands |
| Execution | `finnhub:quote`, `finnhub:insider-transactions`, `finnhub:news-sentiment` | Structured JSON from Finnhub REST |

The repository ships a reference manifest at `manifests/finnhub.toml` with `openapi_max_operations = 50`. That cap keeps the first 50 operations from the spec (by path iteration order) and still includes `news-sentiment` (#11), `insider-transactions` (#24), and `quote` (#44). A fresh `import-openapi` run does not add the cap unless you set it manually.

## Prerequisites

<Note>
Finnhub requires a free API key from [finnhub.io](https://finnhub.io/). `news-sentiment` is US-companies only per the OpenAPI description.
</Note>

- ATI installed and initialized (`ati init` creates `~/.ati/`)
- Network access to `https://finnhub.io/api/v1`
- For `ati assist`: a configured LLM backend (cloud keys) or `--local` / `ATI_ASSIST_PROVIDER=local` per `docs/assist-examples.md`

## End-to-end flow

```mermaid
sequenceDiagram
    participant Agent
    participant ATI as ati CLI
    participant Dir as ~/.ati/
    participant FH as finnhub.io/api/v1

    Agent->>ATI: provider import-openapi spec URL
    ATI->>Dir: specs/finnhub.json + manifests/finnhub.toml
    Agent->>ATI: key set finnhub_api_key
    ATI->>Dir: keyring.enc
    Agent->>ATI: assist finnhub "research AAPL…"
    ATI->>ATI: ManifestRegistry + LLM (/help local or proxy)
    ATI-->>Agent: finnhub:quote / insider / news-sentiment commands
    Agent->>ATI: run finnhub:quote --symbol AAPL
    ATI->>Dir: decrypt key, classify query params
    ATI->>FH: GET /quote?symbol=AAPL&token=***
    FH-->>ATI: Quote JSON
    ATI-->>Agent: text / json / table output
```

<Steps>
<Step title="Import the Finnhub OpenAPI spec">

Download the spec and generate the provider manifest. Provider name defaults from the URL (`finnhub`); auth is auto-detected as query key `token` with keyring name `finnhub_api_key`.

```bash
ati provider import-openapi https://finnhub.io/api/v1/openapi.json
# or preview without writing:
ati provider import-openapi https://finnhub.io/api/v1/openapi.json --dry-run
```

Optional flags from `ati provider import-openapi`:

<ParamField body="--name" type="string">
Override auto-derived provider name (default: `finnhub`).
</ParamField>

<ParamField body="--auth-key" type="string">
Override keyring key name (default: `{name}_api_key` → `finnhub_api_key`).
</ParamField>

<ParamField body="--include-tags" type="string[]">
Only register operations matching these OpenAPI tags.
</ParamField>

<ParamField body="--dry-run" type="boolean">
Print generated TOML and operation count without saving.
</ParamField>

Verify registration:

```bash
ati provider info finnhub
ati tool list --provider finnhub | head -10
ati provider inspect-openapi ~/.ati/specs/finnhub.json
```

</Step>

<Step title="Store the API key">

```bash
ati key set finnhub_api_key "YOUR_FINNHUB_TOKEN"
ati key list   # masked values
```

Alternative without keyring write:

```bash
export ATI_KEY_FINNHUB_API_KEY="YOUR_FINNHUB_TOKEN"
```

The bundled manifest uses `auth_type = "query"`, `auth_query_name = "token"`, and `auth_key_name = "finnhub_api_key"`.

</Step>

<Step title="Discover the research chain with assist">

Scoped assist limits context to Finnhub tools and returns runnable commands:

```bash
ati assist finnhub "research Apple stock — price, insider activity, and sentiment"
```

Typical assist output (from README) orders:

1. `ati run finnhub:quote --symbol AAPL`
2. `ati run finnhub:insider-transactions --symbol AAPL`
3. `ati run finnhub:news-sentiment --symbol AAPL`

`ati assist` auto-detects proxy mode when `ATI_PROXY_URL` is set and forwards to `/help`; otherwise it loads the local registry and calls the internal LLM provider.

</Step>

<Step title="Run the three-tool research chain">

```bash
SYMBOL=AAPL

ati run finnhub:quote --symbol "$SYMBOL"
ati run finnhub:insider-transactions --symbol "$SYMBOL"
ati run finnhub:news-sentiment --symbol "$SYMBOL"
```

For machine-readable output across the chain:

```bash
ati --output json run finnhub:quote --symbol AAPL
ati --output json run finnhub:insider-transactions --symbol AAPL
ati --output json run finnhub:news-sentiment --symbol AAPL
```

<Tip>
Tool names use colon namespacing (`finnhub:quote`). The proxy also accepts underscore form (`finnhub_quote`) by resolving the first underscore to a colon.
</Tip>

</Step>
</Steps>

## Provider manifest (reference)

The committed manifest matches what `import-openapi` generates, plus an operation cap:

```toml
[provider]
name = "finnhub"
handler = "openapi"
base_url = "https://finnhub.io/api/v1"
openapi_spec = "finnhub.json"
auth_type = "query"
auth_query_name = "token"
auth_key_name = "finnhub_api_key"
category = "finance"
openapi_max_operations = 50
```

At load time, `ManifestRegistry` reads `~/.ati/specs/finnhub.json` (sibling of `manifests/`), applies filters including `openapi_max_operations`, and registers tools as `{provider}:{operationId}` with `x-ati-param-location` metadata on each schema property for query/path/header/body routing.

## Research tools

| ATI tool | HTTP | Required args | Notes |
|----------|------|---------------|-------|
| `finnhub:quote` | `GET /quote` | `--symbol` | Real-time US quote; avoid constant polling |
| `finnhub:insider-transactions` | `GET /stock/insider-transactions` | `--symbol` | Max 100 transactions per call; optional `--from`, `--to` (dates) |
| `finnhub:news-sentiment` | `GET /news-sentiment` | `--symbol` | US companies only |

Example invocations with optional date window on insiders:

```bash
ati run finnhub:insider-transactions --symbol AAPL --from 2025-01-01 --to 2025-12-31
```

## Expected response fields

### `finnhub:quote` → `Quote`

Flat object; default text output prints key/value lines.

| Field | Type | Meaning |
|-------|------|---------|
| `c` | number | Current price |
| `d` | number | Change |
| `dp` | number | Percent change |
| `h` | number | Day high |
| `l` | number | Day low |
| `o` | number | Open |
| `pc` | number | Previous close |

<ResponseExample>

```json
{
  "c": 262.52,
  "d": -1.23,
  "dp": -0.4664,
  "h": 266.15,
  "l": 261.43,
  "o": 264.65,
  "pc": 263.75
}
```

</ResponseExample>

### `finnhub:insider-transactions` → `InsiderTransactions`

| Field | Type | Meaning |
|-------|------|---------|
| `symbol` | string | Company symbol |
| `data` | array | Insider transaction rows |

Each `data[]` element (`Transactions`):

| Field | Type | Meaning |
|-------|------|---------|
| `name` | string | Insider name |
| `symbol` | string | Symbol |
| `share` | integer | Shares held after transaction |
| `change` | integer | Share change (positive buy, negative sell) |
| `transactionCode` | string | SEC Form 4 code (e.g. `S` sell, `P` purchase) |
| `transactionPrice` | number | Average transaction price |
| `transactionDate` | string (date) | Transaction date |
| `filingDate` | string (date) | Filing date |

<ResponseExample>

```json
{
  "symbol": "AAPL",
  "data": [
    {
      "name": "COOK TIMOTHY D",
      "symbol": "AAPL",
      "share": 3280295,
      "change": -59751,
      "transactionCode": "S",
      "transactionPrice": 257.57,
      "filingDate": "2025-10-03",
      "transactionDate": "2025-10-01"
    }
  ]
}
```

</ResponseExample>

### `finnhub:news-sentiment` → `NewsSentiment`

| Field | Type | Meaning |
|-------|------|---------|
| `symbol` | string | Requested symbol |
| `companyNewsScore` | number | Company news score |
| `sectorAverageNewsScore` | number | Sector average news score |
| `sectorAverageBullishPercent` | number | Sector average bullish percent |
| `sentiment` | object | `bullishPercent`, `bearishPercent` |
| `buzz` | object | `articlesInLastWeek`, `buzz`, `weeklyAverage` |

<ResponseExample>

```json
{
  "symbol": "AAPL",
  "companyNewsScore": 0.92,
  "sectorAverageNewsScore": 0.58,
  "sectorAverageBullishPercent": 0.62,
  "sentiment": {
    "bullishPercent": 0.71,
    "bearishPercent": 0.29
  },
  "buzz": {
    "articlesInLastWeek": 145,
    "buzz": 1.25,
    "weeklyAverage": 116
  }
}
```

</ResponseExample>

<Warning>
Example numeric values are illustrative shapes. Live values depend on Finnhub market data at call time.
</Warning>

## Chaining pattern for agents

A minimal shell-first pipeline passes the same symbol through all three endpoints and keeps JSON for downstream parsing:

```bash
#!/usr/bin/env bash
set -euo pipefail
SYMBOL="${1:-AAPL}"

quote_json=$(ati --output json run finnhub:quote --symbol "$SYMBOL")
insider_json=$(ati --output json run finnhub:insider-transactions --symbol "$SYMBOL")
sentiment_json=$(ati --output json run finnhub:news-sentiment --symbol "$SYMBOL")

# Agent consumes three JSON blobs (price, insiders, sentiment)
printf '%s\n%s\n%s\n' "$quote_json" "$insider_json" "$sentiment_json"
```

In proxy mode, the same commands run unchanged with `ATI_PROXY_URL` and a JWT carrying scopes such as `tool:finnhub:*` and `help`.

## Operation cap and full catalog

| Configuration | Registered tools | Use when |
|---------------|------------------|----------|
| `import-openapi` (default manifest) | All ~110 operations | Full Finnhub surface |
| `openapi_max_operations = 50` (repo `manifests/finnhub.toml`) | First 50 by spec path order | Smaller agent context |

To raise the cap after import, edit `~/.ati/manifests/finnhub.toml`:

```toml
openapi_max_operations = 110   # or remove the line for unlimited
```

Or filter at import time:

```bash
ati provider import-openapi https://finnhub.io/api/v1/openapi.json --include-tags Default
```

## Troubleshooting

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Tool not found | Provider not imported or cap excludes operation | `ati tool search quote`; adjust `openapi_max_operations` |
| 401 / invalid token | Missing or wrong key | `ati key set finnhub_api_key …` or `ATI_KEY_FINNHUB_API_KEY` |
| Empty `data` on insiders | Symbol or date range | Confirm `--symbol`; try wider `--from` / `--to` |
| `news-sentiment` error | Non-US symbol | Use a US-listed ticker |
| Assist unavailable | No LLM credentials | Configure cloud keys or `ati assist --local` |

## Related pages

<CardGroup>
<Card title="Import OpenAPI providers" href="/import-openapi">
Spec download, auth detection, `x-ati-param-location`, and operation filters.
</Card>
<Card title="Quickstart" href="/quickstart">
`ati init`, first provider, key storage, and verification with `ati tool info`.
</Card>
<Card title="Assist and plan reference" href="/assist-and-plan-reference">
`ati assist` flags, scoped queries, plan mode, and LLM backends.
</Card>
<Card title="Providers and handlers" href="/providers-and-handlers">
OpenAPI handler dispatch, `finnhub:tool` naming, and auth injection.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
`ati key set`, proxy tokens, and scoped `tool:finnhub:*` access.
</Card>
</CardGroup>

---

## 22. Agent harness sandbox

> Shell-first integration pattern across SDK examples, `AtiOrchestrator.provision_sandbox`, scoped env injection, and proxy-mode agent loops without custom tool wrappers.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/22-agent-harness-sandbox.md
- Generated: 2026-06-02T03:19:49.119Z

### Source Files

- `examples/README.md`
- `examples/claude-agent-sdk/openapi_agent.py`
- `examples/openai-agents-sdk/openapi_agent.py`
- `ati-client/python/README.md`
- `ati-client/python/src/ati/__init__.py`
- `ati-client/python/tests/test_orchestrator.py`
- `scripts/test_proxy_e2e.sh`

---
title: "Agent harness sandbox"
description: "Shell-first integration pattern across SDK examples, `AtiOrchestrator.provision_sandbox`, scoped env injection, and proxy-mode agent loops without custom tool wrappers."
---

ATI integrates with agent harnesses by giving the model a shell (or shell-equivalent) tool and a system prompt that documents `ati` CLI commands. The `ati` binary handles discovery, auth, protocol bridging, and response formatting; the harness never wraps individual ATI tools in SDK-specific `@tool` decorators. Production sandboxes add `AtiOrchestrator.provision_sandbox` to inject `ATI_PROXY_URL` and a scoped `ATI_SESSION_TOKEN` so the agent loop runs in proxy mode with zero local credentials.

## Architecture

```mermaid
flowchart TB
  subgraph harness["Agent harness (any SDK)"]
    SP["System prompt: ati assist / tool search / run"]
    SH["Shell tool: Bash, run_shell, ShellTool, bashTool"]
  end

  subgraph local["Local sandbox"]
    ATI_L["ati binary"]
    MAN["manifests/ + optional keyring"]
  end

  subgraph proxy["Proxy sandbox"]
    ATI_P["ati binary (no secrets)"]
    ORCH["AtiOrchestrator.provision_sandbox"]
    PX["ati proxy"]
    KR["keyring + upstream APIs"]
  end

  SP --> SH
  SH -->|"ati run …"| ATI_L
  SH -->|"ati run …"| ATI_P
  ORCH -->|"ATI_PROXY_URL + ATI_SESSION_TOKEN"| ATI_P
  ATI_L --> MAN
  ATI_P -->|"POST /call + Bearer JWT"| PX
  PX --> KR
```

| Layer | Owns | Does not own |
|-------|------|----------------|
| Harness | Model API key, shell execution, system prompt | Provider credentials, MCP/OpenAPI wiring |
| `ati` CLI | Tool dispatch, scope checks (local), proxy routing | Agent reasoning |
| `ati proxy` | Secrets, JWT validation, upstream calls | Custom per-tool SDK adapters |

## Shell-first pattern

The contract is fixed across every example under `examples/`:

1. Expose exactly one shell capability to the model (built-in or a thin `run_shell` wrapper).
2. Document ATI commands in the system prompt: `ati assist`, `ati tool search`, `ati tool info`, `ati run`.
3. Set `ATI_DIR` so subprocesses resolve manifests (repo root in demos, `~/.ati/` in deployments).

The agent discovers tools at runtime and calls them the same way a human would from a terminal. MCP, OpenAPI, and hand-written HTTP providers all surface as `ati run <name> --arg value`; the agent does not branch on provider type.

<Note>
Examples intentionally avoid wrapping ATI in per-provider SDK tools. That keeps harness code portable across model vendors and lets you add providers by editing manifests on the proxy, not agent code.
</Note>

### SDK shell mechanisms

| SDK | Directory | Shell mechanism | Model env for ATI |
|-----|-----------|-----------------|-------------------|
| Claude Agent SDK | `examples/claude-agent-sdk/` | Built-in `Bash` (`allowed_tools=["Bash"]`) | `env={"ATI_DIR": ATI_DIR}` |
| OpenAI Agents SDK | `examples/openai-agents-sdk/` | `@function_tool` `run_shell` | `env={**os.environ, "ATI_DIR": ATI_DIR}` in subprocess |
| LangChain/LangGraph | `examples/langchain/` | `ShellTool` | `os.environ["ATI_DIR"] = ATI_DIR` |
| Google ADK | `examples/google-adk/` | `run_shell()` function tool | `env={**os.environ, "ATI_DIR": ATI_DIR}` |
| Codex CLI | `examples/codex/` | Codex is already a shell agent (`AGENTS.md`) | `export ATI_DIR=…` |
| Pi | `examples/pi/` | `createBashTool(cwd)` | `process.env.ATI_DIR = ATI_DIR` |

Each SDK directory ships `mcp_agent` and `openapi_agent` variants that exercise DeepWiki (MCP) and Crossref/arXiv/Hacker News (OpenAPI + HTTP) with the same prompt structure.

## Local sandbox (development)

Local mode is the default when `ATI_PROXY_URL` is unset. The harness only needs:

- `ati` on `PATH` (typically `cargo build --release` then `export PATH=…/target/release:$PATH`)
- `ATI_DIR` pointing at a tree with `manifests/` (examples default to the repo root)

```bash
export ATI_DIR=/path/to/ati
export PATH="/path/to/ati/target/release:$PATH"
export ANTHROPIC_API_KEY="..."   # or OPENAI_API_KEY, GOOGLE_API_KEY

cd examples/claude-agent-sdk
pip install -r requirements.txt
python mcp_agent.py
```

Claude Agent SDK wiring is representative: `ClaudeAgentOptions` sets `allowed_tools=["Bash"]`, embeds ATI command documentation in `system_prompt`, and passes `env={"ATI_DIR": ATI_DIR}`. OpenAI’s example adds `run_shell` that forwards commands with the same `ATI_DIR` merge pattern.

<Warning>
Local mode may load `keyring.enc` when tools require credentials. Treat local sandboxes as dev-only unless you also configure JWT validation and scoped tokens (see execution modes).
</Warning>

## Proxy sandbox (production)

Proxy mode activates when the agent environment includes `ATI_PROXY_URL`. Every `ati run`, `ati assist`, and scope-aware listing command forwards to the proxy; the sandbox binary holds no API keys.

```mermaid
sequenceDiagram
  participant Orch as Orchestrator
  participant SB as Agent sandbox
  participant CLI as ati CLI
  participant PX as ati proxy

  Orch->>Orch: provision_sandbox(agent_id, tools, skills)
  Orch->>SB: inject ATI_PROXY_URL, ATI_SESSION_TOKEN
  SB->>CLI: Bash: ati run finnhub_quote ...
  CLI->>CLI: detect ATI_PROXY_URL → proxy mode
  CLI->>PX: POST /call + Authorization Bearer JWT
  PX->>PX: validate scope, inject auth, call upstream
  PX-->>CLI: {result, error}
  CLI-->>SB: formatted stdout
```

### Auto-detection in `ati run`

The CLI checks `ATI_PROXY_URL` before loading local manifests. When set, execution uses the proxy path; with `--verbose`, logs include `mode: proxy`. Without it, execution uses `mode: local` (keyring + direct HTTP). The same rule applies to `ati assist` and read-only tool/skill listing paths.

Integration tests confirm: a proxy URL routes to `POST /call` even when `ATI_DIR` has no manifests; omitting `ATI_PROXY_URL` forces local manifest resolution.

### Scoped env injection

`AtiOrchestrator` in `ati-client` (`pip install ati-client`) is the supported provisioning API:

```python
from ati import AtiOrchestrator

orch = AtiOrchestrator(
    proxy_url="https://ati-proxy.example.com",
    secret="<64-char-hex-hs256-secret>",
)

env_vars = orch.provision_sandbox(
    agent_id=f"sandbox:{sandbox_id}",
    tools=["finnhub_quote", "web_search", "github:*"],
    skills=["financial-analysis"],
    extra_scopes=["help"],
    ttl_seconds=7200,
    rate={"tool:github:*": "10/hour"},
    customer_id=None,  # optional tenant for per-customer credentials
    fetch_skill_content=False,
)
# {"ATI_PROXY_URL": "...", "ATI_SESSION_TOKEN": "eyJ..."}
```

| `provision_sandbox` input | Effect |
|---------------------------|--------|
| `agent_id` | JWT `sub` (e.g. `sandbox:abc123`) |
| `tools` | Becomes `tool:<name>` scopes; supports wildcards (`github:*`) |
| `skills` | Becomes `skill:<name>` scopes |
| `extra_scopes` | Raw scope strings (e.g. `help`) |
| `ttl_seconds` | Token lifetime (default `3600`) |
| `rate` | Optional per-tool limits in JWT `ati.rate` |
| `customer_id` | Proxy resolves tenant-specific credentials when set |
| `fetch_skill_content` | If `True`, adds `"skills": {name: SKILL.md}` via `POST /skills/resolve` |

Inject the returned dict into the agent process environment (container env, SDK `env=`, or supervisor-written files). The agent still calls `ati` via shell; only the two ATI variables change behavior.

### Session token resolution and rotation

The proxy client attaches `Authorization: Bearer <token>` from `resolve_session_token()`, which reads in order:

1. `ATI_SESSION_TOKEN` env var
2. `ATI_SESSION_TOKEN_FILE` (or default `/run/ati/session_token`)

Supervisors can rotate JWTs by rewriting the file; the next `ati` invocation picks up the new token without restarting the agent process. Per-provider overrides use manifest `auth_session_token_env` (e.g. `PARCHA_TOOLS_SESSION_TOKEN`) with fallback to `ATI_SESSION_TOKEN`.

<ParamField body="ATI_PROXY_URL" type="string" required>
Base URL of `ati proxy` (no trailing slash required; orchestrator strips it). When set, sandbox needs no local manifests or keyring for tool execution.
</ParamField>

<ParamField body="ATI_SESSION_TOKEN" type="string" required>
HS256 (or proxy-configured) JWT whose `scope` claim limits tools, skills, and help. Required for authenticated proxy routes.
</ParamField>

## System prompt and skills

Harness prompts should document the stable CLI surface:

```bash
ati assist "natural language goal"
ati tool search "keyword"
ati tool info <tool_name>
ati run <tool_name> --key value
ati skill fetch read <skill_name>    # proxy / skillati deployments
```

For proxy deployments with many skills, call `AtiOrchestrator.build_skill_listing(token=env_vars["ATI_SESSION_TOKEN"])` and append the returned `<system-reminder>` block to the system prompt. The proxy filters entries via `GET /skillati/catalog` using JWT scopes; formatting mirrors Claude Code’s skill listing budgets (250 chars per description, 8000 chars body) and points agents at `ati skill fetch read <name>` via Bash.

Deprecated helpers `build_skill_instructions` / `AtiOrchestrator.build_skill_instructions` emit warnings; prefer `build_skill_listing`.

Optional provisioning paths:

- `fetch_skill_content=True` on `provision_sandbox` — inline SKILL.md map in the env dict
- `download_skill` / `download_skills` — write full skill trees to `.claude/skills/` for offline-style layouts

## Agent loop without custom wrappers

A typical turn sequence:

| Step | Command | Purpose |
|------|---------|---------|
| 1 | `ati assist "…"` | LLM-backed tool recommendations (routes to proxy `/help` when `ATI_PROXY_URL` set) |
| 2 | `ati tool search "…"` | Keyword discovery within JWT scopes |
| 3 | `ati tool info <name>` | Schema before calling |
| 4 | `ati run <name> --arg val` | Execute; proxy returns JSON/text for the model |

`scripts/test_proxy_e2e.sh` exercises proxy routing end-to-end: with `ATI_PROXY_URL` pointed at a mock server, `ati run` and `ati assist` return proxy-shaped responses; without it, the CLI attempts local manifest loading.

## Verification checklist

<Steps>
<Step title="Build and path">
Run `cargo build --release`, put `ati` on `PATH`, set `ATI_DIR` to a directory with `manifests/`.
</Step>
<Step title="Local example">
From `examples/claude-agent-sdk`, run `python mcp_agent.py` and confirm Bash lines show `ati tool search` / `ati run`.
</Step>
<Step title="Proxy mock">
Run `bash scripts/test_proxy_e2e.sh` — expects `proxy_mode` in JSON output and `mode: proxy` under `--verbose`.
</Step>
<Step title="Orchestrator unit tests">
In `ati-client/python`, run `pytest` — `test_provision_returns_env_vars`, scope/rate claims, and `build_skill_listing` HTTP mocking.
</Step>
</Steps>

## Failure modes

| Symptom | Likely cause | Mitigation |
|---------|--------------|------------|
| `unknown tool` / manifest errors with proxy URL unset | Local mode, missing `ATI_DIR` manifests | Set `ATI_DIR` or switch to proxy env |
| `ATI_SESSION_TOKEN is required` | Proxy URL set, no bearer token | Call `provision_sandbox` or set token env/file |
| `Invalid ATI_SESSION_TOKEN` | Expired JWT or wrong secret | Re-provision; use `ATI_SESSION_TOKEN_FILE` rotation |
| Empty skill listing | Token scopes exclude skills | Widen `skills=` in `provision_sandbox`; check proxy catalog |
| Tool allowed in prompt but denied at runtime | Scope mismatch (`tool:` prefix, wildcards) | Align `tools=` list with `build_scope_string` rules |

## When to use which mode

| Mode | Credentials | Best for |
|------|-------------|----------|
| Local + `ATI_DIR` | May use local keyring | SDK examples, laptop dev, unauthenticated demo tools |
| Proxy + orchestrator env | Only on proxy host | E2B, Daytona, K8s job, any untrusted agent sandbox |
| Proxy + local JWT enforcement | Keyring on proxy, scoped token in agent | Staged environments mimicking production scopes |

## Related pages

<CardGroup>
<Card title="Execution modes" href="/execution-modes">
Local vs proxy credential placement and threat-model trade-offs.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
Token issuance, `ati token` commands, and orchestrator secrets.
</Card>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
Run `ati proxy`, health checks, and production binding.
</Card>
<Card title="Environment variables" href="/environment-variables">
Full runtime contract for `ATI_PROXY_URL`, `ATI_SESSION_TOKEN`, and `ATI_DIR`.
</Card>
<Card title="JWT and scopes reference" href="/jwt-scopes-reference">
Scope grammar for `provision_sandbox` tool and skill lists.
</Card>
<Card title="Skills and SkillATI" href="/skills-and-skillati">
Progressive disclosure and `build_skill_listing` behavior.
</Card>
<Card title="Quickstart" href="/quickstart">
Initialize `~/.ati/` and first local `ati run` before wiring a harness.
</Card>
</CardGroup>

---

## 23. Security and production

> Threat model, proxy hardening (JWT enforce, download allowlist, SSRF), sig-verify modes, optional Postgres audit/virtual keys, OTel observability, and edge keyring rotation.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/23-security-and-production.md
- Generated: 2026-06-02T03:19:57.508Z

### Source Files

- `docs/SECURITY.md`
- `src/core/sig_verify.rs`
- `src/core/file_manager.rs`
- `docs/PERSISTENCE.md`
- `docs/OTEL.md`
- `src/cli/edge.rs`
- `migrations/20260501000001_init_persistence.sql`

---
title: "Security and production"
description: "Threat model, proxy hardening (JWT enforce, download allowlist, SSRF), sig-verify modes, optional Postgres audit/virtual keys, OTel observability, and edge keyring rotation."
---

ATI production security centers on **where credentials live** (sandbox keyring vs proxy host), **how every request is authenticated** (JWT, optional DB-backed `Ati-Key`, HMAC sig-verify for passthrough), and **what egress the proxy allows** (SSRF checks and download host allowlists on `file_manager:download`). The sections below map those controls to exact env vars, CLI flags, and operator workflows.

## Threat model and trust boundaries

ATI exposes tools to untrusted agent sandboxes. The security model splits on execution mode:

| Mode | Credential location | Primary risk |
|------|---------------------|--------------|
| **Local** (`ATI_PROXY_URL` unset) | AES-256-GCM `keyring.enc` decrypted in-process; one-shot session key at `/run/ati/.key` (unlinked after first read) | Keys exist inside the sandbox process (`mlock`, `madvise(DONTDUMP)`, zeroize on drop — best-effort) |
| **Proxy** (`ATI_PROXY_URL` set) | Real API keys only on the proxy host | Sandbox holds manifests, scopes, and `ATI_SESSION_TOKEN` — no keyring material |

```mermaid
flowchart TB
  subgraph sandbox["Agent sandbox"]
    ATI_CLI["ati run / ati tool"]
    TOKEN["ATI_SESSION_TOKEN JWT or Ati-Key"]
  end
  subgraph proxy_host["Proxy host (~/.ati/)"]
    PROXY["ati proxy"]
    KEYRING["keyring.enc + manifests"]
    DB["Postgres optional"]
  end
  subgraph upstream["Upstream APIs"]
    APIs["HTTP / MCP / OpenAPI providers"]
  end
  ATI_CLI -->|"POST /call (proxy mode)"| PROXY
  TOKEN --> PROXY
  PROXY --> KEYRING
  PROXY --> DB
  PROXY --> APIs
```

**Local-mode mitigations** include: no secrets in environment variables; session key file deleted after first read; encrypted keyring at rest; `mlock` / `MADV_DONTDUMP` on secret pages. **Known limitations**: without seccomp, `ptrace` can read process memory; brief race on `/run/ati/.key` before first read; `mlock` may fail silently if `RLIMIT_MEMLOCK` is low.

**Proxy-mode mitigations** include: zero key material in the sandbox; JWT scope enforcement on `/call`, `/mcp`, discovery routes, and SkillATI; scope-filtered listings so agents cannot enumerate tools they are not allowed to use. **Known limitations**: `ATI_PROXY_URL` is visible in the environment (not secret); proxy host compromise equals credential compromise; added network latency.

Scope behavior is **strict when JWT is configured** and **unrestricted dev mode when it is not** — production proxies must configure `ATI_JWT_PUBLIC_KEY` or `ATI_JWT_SECRET` so `auth_middleware` rejects missing or invalid `Authorization: Bearer` tokens.

## Production proxy hardening

Treat the following as a minimum bar for an Internet-facing `ati proxy`:

<Steps>
<Step title="Enable JWT validation">
Set ES256 or HS256 verification env vars (`ATI_JWT_PUBLIC_KEY`, or `ATI_JWT_SECRET`, plus optional `ATI_JWT_ISSUER` / `ATI_JWT_AUDIENCE`). Issue scoped tokens with `ati token issue` and pass them to sandboxes as `ATI_SESSION_TOKEN`.

Verify `/health` reports `"auth": "jwt"` (not `"disabled"`).
</Step>
<Step title="Restrict file_manager downloads">
Set both egress controls:

```bash
export ATI_SSRF_PROTECTION=1
export ATI_DOWNLOAD_ALLOWLIST=v3b.fal.media,*.googleapis.com,raw.githubusercontent.com
```

`file_manager:download` always runs SSRF validation via `validate_url_not_private` before fetch. The allowlist is separate: when `ATI_DOWNLOAD_ALLOWLIST` is **unset**, any non-private host is permitted — acceptable only for local dev.
</Step>
<Step title="Roll out HMAC sig-verify">
For passthrough or edge deployments, load `sandbox_signing_shared_secret` into the keyring, start with `--sig-verify-mode log`, observe structured `sig_verify` logs, then switch to `enforce` (see below).
</Step>
<Step title="Optional Postgres for virtual keys">
Build with `--features db`, set `ATI_DB_URL`, run `ati proxy --migrate`, and set `ATI_ADMIN_TOKEN` for `/admin/keys/*`. Use `Authorization: Ati-Key` for per-job credentials with immediate revocation.
</Step>
</Steps>

### JWT enforcement

When `jwt_config` is present at proxy startup, `auth_middleware` requires `Authorization: Bearer <jwt>` on all routes except:

- `/health` and `/.well-known/jwks.json`
- Passthrough routes matched by `PassthroughRouter` (authenticated by sig-verify instead)

| Claim / behavior | Effect |
|------------------|--------|
| `scope` (space-delimited) | Tool, skill, provider, category wildcards; `help` scope for `/help` |
| `exp` | Hard expiry — expired tokens rejected |
| Legacy `tool:github_search` | Normalized to colon namespaced form without weakening checks |

`Authorization: Ati-Key <raw>` is evaluated **before** JWT when both are present, so per-job DB keys get correct audit attribution.

<Warning>
If JWT keys are not configured, the proxy runs in **unrestricted dev mode** and allows unauthenticated requests (unless an `Ati-Key` header is sent and the `db` feature is enabled).
</Warning>

### Download allowlist (`ATI_DOWNLOAD_ALLOWLIST`)

Comma-separated host patterns (case-insensitive). Pattern forms:

| Pattern | Matches |
|---------|---------|
| `example.com` | Exact host |
| `*.fal.media` | `v3b.fal.media`, `cdn.fal.media`, and `fal.media` — not suffix tricks like `evilfal.media` |
| `*` | Everything (defeats the allowlist) |

Violations return HTTP **403** with `HostNotAllowed` on `POST /call` for `file_manager:download`.

### SSRF protection (`ATI_SSRF_PROTECTION`)

Used by the generic HTTP executor (`core/http.rs`) for hand-written and OpenAPI-backed HTTP tools:

| Value | Behavior |
|-------|----------|
| unset | No SSRF check (dev default) |
| `warn` | Log warning, allow request |
| `1` or `true` | Block loopback, RFC1918, link-local, `.internal`, `.local`, and DNS-resolved private IPs |

`file_manager:download` applies SSRF checks **regardless** of this env var. MCP and CLI subprocess tools are outside this SSRF path today.

## HMAC sandbox signature verification

Sig-verify is axum middleware on every non-exempt path. It validates outbound sandbox traffic using a shared secret from the keyring entry `sandbox_signing_shared_secret` (hex-decoded when valid hex, otherwise raw UTF-8 bytes).

### Signing format

Clients send:

```http
X-Sandbox-Signature: t=<unix_ts>,s=<hex_hmac>
```

Optional `X-Sandbox-Job-Id` is logged only. HMAC-SHA256 covers `{t}.{method}.{path}` (constant-time compare on digest).

### Modes (`--sig-verify-mode`)

| Mode | Request handling | Response |
|------|------------------|----------|
| `log` (default) | Always allow | Structured log: `valid`, `reason`, `job_id`, `method`, `path` |
| `warn` | Always allow | Adds `X-Signature-Status: <reason>` |
| `enforce` | **403 Forbidden** on invalid/missing signature | Body is the reason string (`missing_signature`, `hmac_mismatch`, `expired_timestamp_drift=Ns`, etc.) |

<ParamField body="--sig-drift-seconds" type="integer" default="60">
Maximum `|now - t|` in seconds before `ExpiredTimestamp`.
</ParamField>

<ParamField body="--sig-exempt-paths" type="string">
Comma-separated globs. Defaults include `/health`, `/healthz`, `/root/*`, `/npm/*`, `/otel/*`, `/.well-known/jwks.json`.
</ParamField>

Startup gates:

- **`enforce` without secret** — proxy refuses to start (every request would 403).
- **Passthrough + `log`/`warn` without secret** — error log: passthrough is effectively unauthenticated.

On **SIGHUP**, the proxy reloads `keyring.enc` and hot-swaps the sig-verify secret via `ArcSwapOption` without restart. Transient reload failures **preserve** the previous secret so a disk glitch does not mass-403 traffic in enforce mode.

```text
Request path (non-exempt):
  sig_verify_middleware → auth_middleware → handler
Passthrough (host+path match, not a named ATI route):
  sig_verify only (JWT skipped)
```

## Optional Postgres persistence

Persistence is **compile-time** (`--features db`) and **runtime** (`ATI_DB_URL`) gated. Without both, behavior matches a non-DB proxy.

### Schema and current write paths

Migration `20260501000001_init_persistence.sql` creates:

| Table | Purpose | Written today |
|-------|---------|---------------|
| `ati_keys` | Virtual keys (scope arrays, TTL, counters) | Yes — via `KeyStore` |
| `ati_deleted_keys` | Soft-delete snapshots on revoke | Yes |
| `ati_audit_log` | Admin mutations (`key.create`, `key.revoke`, …) | Yes |
| `ati_call_log` | Per-request proxy audit rows | **Schema only** — proxy audit still appends to `~/.ati/audit.jsonl` via `core/audit` |

<Info>
Per-call Postgres audit (`ati_call_log` inserts on every `/call`) is planned; operators should use `ati audit tail|search` on the JSONL file until DB call logging ships.
</Info>

### Virtual keys (implemented)

Orchestrators issue one-shot credentials:

:::endpoint POST /admin/keys/issue
Requires `ATI_DB_URL`, `db` feature build, and `Authorization: Bearer <ATI_ADMIN_TOKEN>`. Body includes `user_id`, `alias`, scope arrays (`tools`, `providers`, `categories`, `skills`), optional `expires_in_secs`. Response includes `raw_key` (prefix `ati-key_`), `hash`, `alias`, `expires_at` — **raw key is shown once**; DB stores `sha256(raw)` only.
:::

Revocation: `DELETE /admin/keys/{hash}`, bulk revoke, `LISTEN ati_key_revoked` for cross-pod cache invalidation (30s moka TTL on lookups).

Sandboxes authenticate with:

```http
Authorization: Ati-Key ati-key_<random>
```

Claims are synthesized from the row so existing scope checks unchanged.

### Startup and health

| Condition | Result |
|-----------|--------|
| `ATI_DB_URL` unset | `db: "disabled"` in `/health` |
| URL set, DB reachable, `--migrate` | Migrations applied; `db: "connected"` |
| URL set, DB unreachable | Proxy **exits** at startup |
| URL set, build without `db` feature | Proxy **exits** with rebuild instructions |

<Note>
`/health` `db: connected` reflects pool configuration at startup, not continuous Postgres liveness.
</Note>

## Observability (OpenTelemetry)

OTel is behind `--features otel`. When `OTEL_EXPORTER_OTLP_ENDPOINT` is unset, the layer is not installed (zero runtime overhead). When set on a non-otel build, ATI logs a warning to rebuild with the feature.

### Spans and metrics

| Span | Where | Key attributes |
|------|-------|----------------|
| `http.server.request` | Outermost proxy middleware | `http.route`, `http.request.method`, `http.response.status_code`, `url.path` |
| `proxy.call` | `POST /call` | `tool` |
| `proxy.mcp` | `POST /mcp` | `jsonrpc.method` |
| `proxy.help` | `POST /help` | — |
| `passthrough.request` | Passthrough fallback | `route`, `upstream` |

| Metric | Type | Labels |
|--------|------|--------|
| `ati.proxy.requests` | counter | `http.route`, `http.request.method`, `http.response.status_class` |
| `ati.proxy.request_duration_ms` | histogram | same |
| `ati.upstream.errors` | counter | `provider`, `error_kind` |

W3C `traceparent` is extracted on inbound proxy requests and injected on outbound HTTP/MCP HTTP calls. Passthrough strips inbound sandbox trace headers before upstream injection.

Production build pattern:

```bash
cargo build --release --features sentry,otel
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.example.com/otlp
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic ..."
export OTEL_SERVICE_NAME=ati-proxy
export ENVIRONMENT_TIER=production
ati proxy --port 8090
```

`/otel/*` routes are sig-verify exempt (aligned with typical Caddy `forward_auth` skips).

## Edge keyring rotation

`ati edge` commands target static-IP egress VMs that hold the production keyring. They shell out to the 1Password CLI (`op`) — field `label` becomes keyring entry name.

<Steps>
<Step title="Bootstrap a fresh VM">
```bash
ati edge bootstrap-keyring \
  --vault "Production" \
  --item "ATI Edge Keyring" \
  --ati-dir /var/lib/ati \
  --op-token-file "${CREDENTIALS_DIRECTORY}/op-token"
```

Writes `keyring.enc` (AES-256-GCM) and `.keyring-key` (persistent 32-byte session key, mode `0600`) via atomic tempfile + `rename`.
</Step>
<Step title="Rotate credentials without restart">
```bash
ati edge rotate-keyring \
  --vault "Production" \
  --item "ATI Edge Keyring" \
  --ati-dir /var/lib/ati \
  --service ati
```

Re-pulls from 1Password, atomically replaces `keyring.enc` (session key unchanged), sends **SIGHUP** to the systemd `MainPID`. The proxy reloads API keys and `sandbox_signing_shared_secret` for sig-verify. Use `--no-signal` when rotating before first start.
</Step>
</Steps>

Expected 1Password item fields include provider API keys and `sandbox_signing_shared_secret` for HMAC enforcement.

## Binary supply chain

Release verification (from `docs/SECURITY.md`):

```bash
gh attestation verify ./ati --repo Parcha-ai/ati
sha256sum -c ati-x86_64-unknown-linux-musl.sha256
cargo audit bin ./ati
```

CI runs `cargo-deny` (registry allowlist, no git deps, RustSec advisories, license allowlist) and pins GitHub Actions to full commit SHAs.

## Failure modes

| Symptom | Likely cause | Mitigation |
|---------|--------------|------------|
| `401` on all proxy calls | Missing/expired JWT or revoked `Ati-Key` | Re-issue token; check `ati token validate` |
| `403` + `missing_signature` | `enforce` mode without `X-Sandbox-Signature` | Fix sandbox signer or use `log` during rollout |
| `403` + `HostNotAllowed` | URL host outside `ATI_DOWNLOAD_ALLOWLIST` | Add pattern or fix agent URL |
| `403` + `PrivateUrl` | SSRF block on internal target | Expected — do not disable in production |
| Proxy won't start with `ATI_DB_URL` | DB down or non-`db` binary | Fix connectivity or use `-db` release artifact |
| Passthrough works without auth | `log`/`warn` + no signing secret | Set secret + `--sig-verify-mode enforce` |
| Stale API keys after rotation | SIGHUP failed | Check `systemctl` MainPID; restart proxy |

## Related pages

<CardGroup>
<Card title="Execution modes" href="/execution-modes">
Local vs proxy credential placement and mode auto-detection.
</Card>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
Bind, passthrough, sig-verify flags, and systemd deployment patterns.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
Token issuance, JWKS, and orchestrator scope patterns.
</Card>
<Card title="File manager operations" href="/file-manager-operations">
Download/upload tools, allowlist examples, and upload destinations.
</Card>
<Card title="Environment variables" href="/environment-variables">
Full runtime contract including SSRF, OTel, and DB vars.
</Card>
<Card title="Build, test, and troubleshooting" href="/build-test-and-troubleshooting">
Feature flags (`db`, `otel`, `sentry`) and E2E verification.
</Card>
</CardGroup>

---

## 24. Build, test, and troubleshooting

> `cargo build/test` targets, musl release builds, feature flags (`db`, `otel`, `sentry`), E2E shell scripts, live MCP tests, and common failure signals from integration tests.

- Page Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/pages/24-build-test-and-troubleshooting.md
- Generated: 2026-06-02T03:49:49.711Z

### Source Files

- `README.md`
- `Cargo.toml`
- `scripts/test_skills_e2e.sh`
- `scripts/test_proxy_server_e2e.sh`
- `scripts/e2e/README.md`
- `tests/mcp_live_test.rs`
- `src/core/error.rs`

---
title: "Build, test, and troubleshooting"
description: "`cargo build/test` targets, musl release builds, feature flags (`db`, `otel`, `sentry`), E2E shell scripts, live MCP tests, and common failure signals from integration tests."
---

ATI ships as the `agent-tools-interface` Rust crate (`ati` library, `ati` binary) with optional compile-time features, a layered `cargo test` suite (~790 cases), and bash E2E harnesses that exercise real `ati proxy` processes. Release Linux artifacts are static musl binaries; CI runs the default test sweep plus proxy, skills, full-stack, and OTel integration scripts against a debug build.

## Build commands

| Goal | Command | Output |
|------|---------|--------|
| Dev binary | `cargo build` | `target/debug/ati` |
| Optimized binary | `cargo build --release` | `target/release/ati` |
| Static Linux (sandbox) | `cargo build --release --target x86_64-unknown-linux-musl` | `target/x86_64-unknown-linux-musl/release/ati` |
| Install from crates.io | `cargo install agent-tools-interface` | `~/.cargo/bin/ati` |

Linux musl builds need `musl-tools` on x86_64; `aarch64-unknown-linux-musl` release builds use `cross` in `.github/workflows/release.yml`. Tagged releases also run `cargo auditable build` for supply-chain attestations.

The release profile in `Cargo.toml` optimizes for small static binaries: `opt-level = "z"`, `lto = true`, `codegen-units = 1`, `strip = true`.

<CodeGroup>
```bash title="Default build"
cargo build --release
```

```bash title="Production proxy (Sentry + OTel)"
cargo build --release --features sentry,otel
```

```bash title="Proxy with Postgres persistence"
cargo build --release --features db --target x86_64-unknown-linux-musl
```
</CodeGroup>

<ParamField body="--features" type="comma-separated flags">
Optional compile-time features. Default build has no features enabled.
</ParamField>

## Feature flags

| Feature | Enables | Runtime behavior |
|---------|---------|------------------|
| *(default)* | Core CLI + proxy | No Sentry, OTel, or Postgres driver compiled in |
| `db` | `sqlx`, `moka` | `ATI_DB_URL` connects a pool; `--migrate` applies embedded SQL; `/health` reports `db: "connected"` |
| `otel` | OpenTelemetry SDK + OTLP HTTP exporter | Spans/metrics when `OTEL_EXPORTER_OTLP_ENDPOINT` is set; see `docs/OTEL.md` |
| `sentry` | `sentry`, `sentry-tracing` | Error reporting when `SENTRY_DSN` or `GREP_SENTRY_DSN` is set |

<Note>
`db` uses `sqlx` with `runtime-tokio-rustls` only (no MySQL compiled into the binary). `otel` uses HTTP/protobuf OTLP, not gRPC, to keep musl artifacts lean (~500 KiB measured delta in `Cargo.toml` comments).
</Note>

Build without `db` but set `ATI_DB_URL` → `connect_optional()` returns `DbError::FeatureDisabled` with message *"ATI built without `db` feature; rebuild with --features db to use ATI_DB_URL"*. Build without `otel` but set `OTEL_EXPORTER_OTLP_ENDPOINT` → startup warning pointing at `--features otel`. Same pattern for Sentry DSN without `sentry`.

Release matrix (`.github/workflows/release.yml`) publishes musl and macOS variants with suffixes `""`, `-sentry`, and `-sentry-otel`. Pre-release tags (`v0.8.0-rc.*`) upload assets but skip crates.io/PyPI publish.

## Test layers

```mermaid
flowchart TB
  subgraph unit["Unit tests (src/**/mod tests)"]
    U["~304 in lib crate"]
  end
  subgraph integration["Integration tests (tests/*.rs)"]
    I["wiremock + tempfile + tower::ServiceExt"]
    S["assert_cmd + CARGO_BIN_EXE_ati subprocess"]
  end
  subgraph optional["Operator / CI opt-in"]
    L["mcp_live_test --ignored"]
    D["control_plane_db_test --features db"]
    K["keys_live_test + ATI_DB_URL_TEST"]
  end
  subgraph e2e["Bash E2E (scripts/)"]
    E1["test_proxy_e2e.sh"]
    E2["test_skills_e2e.sh"]
    E3["test_proxy_server_e2e.sh"]
    E4["test_full_stack_e2e.sh"]
    E5["test_otel_e2e.sh"]
  end
  unit --> integration
  integration --> e2e
  optional -.-> integration
```

### Default `cargo test`

```bash
cargo test                    # full suite; RUSTFLAGS=-Dwarnings in CI
cargo test --quiet            # CI uses this in .github/workflows/ci.yml
cargo test --test manifest_test
cargo test test_parse_parallel_manifest   # single test by name
cargo test mcp_client         # pattern across crates
```

A clean run reports **~790 passing tests** across the library crate and `tests/*.rs` integration files (README badge: 791). `tests/mcp_live_test.rs` contributes **8 ignored** network tests that do not run in the default sweep.

### Integration test patterns

| Pattern | Where | Purpose |
|---------|-------|---------|
| `wiremock::MockServer` | `tests/proxy_test.rs`, `proxy_server_test.rs`, `http_test.rs`, … | Mock upstream HTTP without TCP |
| `tower::ServiceExt::oneshot` | Proxy router tests | In-process axum handlers, no bind |
| `assert_cmd` + `env!("CARGO_BIN_EXE_ati")` | `skill_fetch_test.rs`, `provider_load_test.rs`, … | Real CLI subprocess with env overrides |
| `tempfile::TempDir` | Most integration tests | Isolated `ATI_DIR` / manifests |
| `tests/common/mod.rs` | Shared builders | `test_provider()`, `temp_manifests()`, JWT helpers |

### Live MCP tests

Network-dependent cases in `tests/mcp_live_test.rs` are `#[ignore]` so CI and local sweeps stay offline-safe.

```bash
cargo test --test mcp_live_test -- --ignored --nocapture
```

<Warning>
Live tests need `npx` in PATH and credentials from env or `~/.claude.json` / `~/.github-token` (GitHub, Linear, Sentry MCP servers). They hit real MCP endpoints and external APIs.
</Warning>

Ignored cases include GitHub stdio (`@modelcontextprotocol/server-github`), Linear HTTP MCP, Sentry MCP, DeepWiki HTTP, and ATI `McpClient` integration paths. Unit-level SSE parsing tests in the same file **are not** ignored and run on every `cargo test`.

### Postgres-backed tests (feature `db`)

```bash
# In-process: no Postgres required
cargo test --test db_optional_test

# Live DB: opt in with ATI_TEST_DB_URL
export ATI_TEST_DB_URL=postgres://postgres:test@localhost:5440/postgres
cargo test --features db --test control_plane_db_test

# Virtual keys E2E
export ATI_DB_URL_TEST=postgres://...
cargo test --features db --test keys_live_test
```

`db_optional_test` verifies `DbState::Disabled` when `ATI_DB_URL` is unset/empty and that migrations are a no-op on disabled state. Live suites skip cleanly when the env URL is missing.

## E2E shell scripts

| Script | What it validates | Binary default | CI |
|--------|-------------------|----------------|-----|
| `scripts/test_proxy_e2e.sh` | `ati run` through mock Python proxy (`ATI_PROXY_URL`) | `./target/debug/ati` | Yes |
| `scripts/test_skills_e2e.sh` | Skill registry, HTTP/OpenAPI/MCP manifests, proxy `/skills` | `target/release/ati` (builds if missing) | Yes (`ATI_BIN=./target/debug/ati`) |
| `scripts/test_proxy_server_e2e.sh` | Real `ati proxy` → mock upstream round-trip | `./target/debug/ati` | Yes |
| `scripts/test_full_stack_e2e.sh` | Sig-verify, SIGHUP rotation, passthrough, WebSocket, `ati edge` (~67 cases) | `target/release/ati` | Yes (needs `websockets==15.0.1`) |
| `scripts/test_otel_e2e.sh` | OTLP HTTP export with `--features otel` | `./target/debug/ati` | Yes |
| `scripts/test_cleanup_e2e.sh` | Audit, rate limiter, plan mode | `target/release/ati` | No (local) |
| `scripts/test_skill_fetch_e2e.sh` | Remote GCS skillati shape | `./target/debug/ati` | No (needs GCP creds) |

<Steps>
<Step title="Run the CI-equivalent E2E stack locally">

```bash
cargo build
bash scripts/test_proxy_e2e.sh
ATI_BIN=./target/debug/ati bash scripts/test_skills_e2e.sh
bash scripts/test_proxy_server_e2e.sh
pip install --user 'websockets==15.0.1'
ATI_BIN=./target/debug/ati bash scripts/test_full_stack_e2e.sh --pr all
bash scripts/test_otel_e2e.sh
```

</Step>
<Step title="Full-stack harness with post-mortem">

```bash
cargo build --release --bin ati
bash scripts/test_full_stack_e2e.sh --pr all --keep-tmpdir
# Inspect /tmp/ati-e2e-XXXXXX proxy logs
```

Filter groups: `--pr 96` (sig-verify), `--pr 97` (`ati edge`), `--pr 98` (WebSocket). See `scripts/e2e/README.md` for port map (18910–18931 on `127.0.0.1`) and scenario matrix.

</Step>
</Steps>

<Tip>
Set `ATI_BIN` to point E2E scripts at a specific build. CI builds debug once, then reuses it for all harnesses except the Sentry smoke build (`target/sentry/debug/ati`) so feature-specific binaries do not overwrite the default E2E binary.
</Tip>

### What full-stack E2E catches that `cargo test` cannot

`scripts/e2e/README.md` documents gaps in-process tests miss: middleware ordering (sig-verify before JWT), real `SIGHUP` + `ArcSwap` secret rotation, 50 concurrent in-flight requests across rotation, atomic `ati edge rotate-keyring`, fake `op` subprocess via `--op-path`, and real WebSocket upgrade/subprotocol negotiation.

## CI jobs

`.github/workflows/ci.yml` runs on every push/PR to `main`:

| Job | Command / action |
|-----|------------------|
| Test and E2E | `cargo test --quiet`, `cargo build`, Sentry smoke, all E2E scripts above |
| cargo-deny | `cargo deny check` |
| Formatting | `cargo fmt --all --check` |
| Clippy | `cargo clippy --all-targets -- -D warnings` |
| Coverage | `cargo llvm-cov` |
| cargo-audit | `cargo audit` |

`SKIP_OTEL_E2E=1` skips the OTel harness when needed.

## Structured errors and exit codes

CLI failures map through `src/core/error.rs`. With `--output json`, stderr emits a structured object; `--verbose` adds an error chain.

| `error.code` | Typical message signal | Exit code |
|--------------|------------------------|-----------|
| `tool.not_found` | `unknown tool` | 1 |
| `auth.scope_denied` | `scope`, `access denied` | 3 |
| `auth.expired` | `expired` | 3 |
| `auth.missing_key` | `key not found`, `missing key`, `no keys found` | 3 |
| `provider.timeout` | `timeout` | 4 |
| `provider.upstream_error` | `upstream`, `bad gateway`, `mcp error` | 4 |
| `provider.not_found` | `provider` + `not found` | 4 |
| `input.missing_arg` | `missing`, `required` | 2 |
| `input.invalid_value` | `invalid`, `parse` | 2 |
| `rate.exceeded` | `rate limit` | 5 |
| `tool.execution_failed` | *(fallback)* | 1 |

<Info>
Use `ati run ... --verbose` for full error chains on stderr. JSON mode always prints `error.code`, `error.message`, and `error.exit_code`; verbose JSON adds `error.chain`.
</Info>

## Troubleshooting

### Build failures

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `linker ... musl-gcc not found` | Missing musl toolchain | `sudo apt-get install musl-tools` (x86_64 Linux) |
| `failed to run custom build command` for `ring` | Cross-compile env | Use release workflow’s `cross` for `aarch64-unknown-linux-musl` |
| `multiple versions of crate opentelemetry` | Mixed OTel crate versions | Bump `opentelemetry`, `opentelemetry_sdk`, `opentelemetry-otlp`, `tracing-opentelemetry` together (`Cargo.toml` note) |
| Clippy CI failure | `RUSTFLAGS=-Dwarnings` | Run `cargo clippy --all-targets -- -D warnings` locally |

### Test failures

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `Address already in use` on 18910–18931 | Stale proxy from aborted E2E | Re-run harness (orchestrator kills ports on trap); or `ss -tln` / `lsof` |
| `python3 -c 'import websockets' failed` | Missing dep for full-stack | `pip install --user websockets==15.0.1` |
| `ATI built without db feature` in proxy logs | `ATI_DB_URL` set on default binary | Rebuild with `--features db` or unset `ATI_DB_URL` |
| `enabled sentry client` grep fails in CI | Built without `sentry` or bad DSN | Build `cargo build --features sentry --target-dir target/sentry` |
| MCP live test skip / fail | Not using `--ignored` or no token | `cargo test --test mcp_live_test -- --ignored`; set `GITHUB_PERSONAL_ACCESS_TOKEN`, `LINEAR_API_KEY`, etc. |
| `connect_optional_set_env_without_feature_errors` | Expected on default build | Either unset `ATI_DB_URL` or build with `--features db` |

### Runtime / operator signals

| Symptom | Check |
|---------|-------|
| Tool missing at runtime | `ati tool list`; manifest under `$ATI_DIR/manifests/`; scope in JWT |
| Proxy 401 vs 403 on passthrough | 403 often sig-verify (unsigned); 401 JWT — full-stack Group F tests ordering |
| `/health` shows `db: "disabled"` | Normal without `ATI_DB_URL`; set URL + `db` feature for persistence |
| OTel spans absent | `OTEL_EXPORTER_OTLP_ENDPOINT`, `--features otel`, 10s flush window (`docs/OTEL.md`) |
| Sentry events dropped on CLI exit | Regression if transport flush skipped; CI greps for `client close; request transport to shut down` |

### Manifest and registry errors

Integration tests in `tests/manifest_test.rs` cover TOML parsing for auth types, OpenAPI overrides, MCP fields, and `auth_generator` blocks. Load-time failures surface as manifest/registry errors before any network call — fix the `.toml` under `manifests/` and re-run `ati tool list` to confirm registration.

## Local quality gates before push

<CodeGroup>
```bash title="Fast loop"
cargo test
cargo fmt --all --check
cargo clippy --all-targets -- -D warnings
```

```bash title="Pre-release proxy check"
cargo build --release --features sentry,otel
bash scripts/test_full_stack_e2e.sh --pr all
```
</CodeGroup>

Set `RUST_LOG=debug` or `ati=debug` for tracing during failing E2E or proxy runs.

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Pre-built musl binaries, `cargo install`, and platform prerequisites.
</Card>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
Run `ati proxy` with optional `--features db` and production flags.
</Card>
<Card title="Environment variables" href="/environment-variables">
`ATI_DB_URL`, OTel, Sentry, JWT, and proxy env contract.
</Card>
<Card title="Security and production" href="/security-and-production">
Threat model, proxy hardening, and observability in production.
</Card>
</CardGroup>

---
