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

- 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

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