# 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.

- Repository: Parcha-ai/ati
- GitHub: https://github.com/Parcha-ai/ati
- Human docs: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa
- Complete Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/llms-full.txt

## 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>
