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

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