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

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