Metadata-Version: 2.4
Name: nf-ai-sandbox-cli
Version: 0.1.0
Summary: CLI for the AI Sandbox Pilot platform (Section 8.2 / Build Order 42)
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: ai-sandbox-platform
Requires-Dist: click<9.0,>=8.1
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == "test"

# nf-ai-sandbox-cli

Installs the `ai-sandbox` command. The distribution is named `nf-ai-sandbox-cli`
because `ai-sandbox-cli` was already taken on PyPI.

Section 8.2 / Build Order 42.

```bash
pip install -e .   # also installs the ai_sandbox SDK as a dependency

ai-sandbox login
ai-sandbox launch --template llama-3-8b
ai-sandbox launch --gpu H100 --provider auto --template llama-3-8b --hours 4
ai-sandbox status <instance-id>
ai-sandbox templates
ai-sandbox whoami
ai-sandbox logout
```

`login` uses the same browser-based device-authorization flow described in
`ai_sandbox`'s README / Section 8.2 — never a manual API key copy-paste.
Credentials (including a refresh token — see Auth refresh below) are stored
at `~/.ai-sandbox/credentials.json` and are shared with any script using
`from ai_sandbox import Client`.

## Commands

| Command | Does |
|---|---|
| `login [--no-browser]` | Device-authorization login. `--no-browser` prints the verification URL instead of opening one. |
| `logout` | Deletes `~/.ai-sandbox/credentials.json`. |
| `whoami` | Shows the logged-in user's email/name. |
| `templates` | Lists the sandbox template catalog. |
| `launch --template <id> [flags]` | Launches a sandbox; blocks and streams progress, then prints the connection URL. `--free-text "<request>"` is the alternative to `--template` for Section 7.4 catalog-miss requests — exactly one of the two is required. `--git-repo`/`--run-command` or `--notebook` runs a workload automatically once the sandbox is up. |
| `status <instance-id>` | Current status + connection details (Jupyter URL / SSH command). |
| `tunnel <instance-id> --port <port> [--local-port <port>]` | SSH-forwards a local port to a port inside the sandbox (TensorBoard, Gradio, MLflow, ...) — see OI-42 note below. Blocks until Ctrl+C. |
| `workload-run <instance-id> [--rerun] [--attach-git-repo <url> --run-command <cmd> [--git-ref <ref>]] [--cancel]` | Checks on a `--git-repo`/`--notebook` workload started by `launch`, re-runs it, attaches a new one to an instance launched with none yet, or cancels the currently attached run. |
| `metrics <instance-id>` | Latest live GPU/CPU/RAM/disk utilization snapshot for a running instance. |
| `metrics-history <instance-id> [--since <iso-timestamp>]` | The raw utilization time series behind `metrics` (charting it is a frontend-only surface, but the data is available here for scripting). |
| `stop <instance-id>` | Pauses a running sandbox, preserving disk/state. |
| `resume <instance-id>` | Un-pauses a grace-stopped (or plain stopped) sandbox — the counterpart to `stop`. |
| `terminate <instance-id>` | Terminates a sandbox permanently. |
| `migrate <instance-id> <target-provider>` | Moves a running sandbox to a different provider — relaunches there and copies the workspace over, then terminates the old instance. |
| `instances` | Every sandbox you've launched, newest first. |
| `organization` | The shared org budget your usage is billed against, if you're a member of one. |
| `usage` | Your usage/billing summary — GPU hours, storage, free-quota-covered spend, and any real card charges. |
| `quota-request --amount <usd> --reason <text>` | Asks an admin to raise your free quota above the platform default (OI-39). |
| `pricing` | Lists every published hourly rate you'd actually pay, cheapest first. |
| `memory [--set-gpu/--set-provider/--set-framework/--set-budget <val>] [--enable/--disable] [--forget]` | Shows or updates what the Copilot remembers about you. With no flags, just shows the current state. |
| `experiments` | Lists every experiment you've saved. |
| `experiment <experiment-id>` | Shows one experiment's config and results. |
| `experiment-rerun <experiment-id>` | Re-launches the sandbox an experiment recorded, from the same config. |
| `experiments-compare <id> <id> [<id> ...]` | Compares 2+ experiments' recorded results side by side. |
| `benchmarks [--type <type>]` | Lists your recorded benchmark results. |
| `benchmark-run <instance-id> --type <type> [--metric-key <key>]` | Runs a benchmark against one running instance. |

Every command accepts `--api-url` (or `AI_SANDBOX_API_URL`) to point at a
non-default backend; otherwise it resolves the same way the SDK's `Client`
does (stored credentials file, then `http://localhost:8000`).

## Reaching a non-Jupyter service in your sandbox (OI-42)

The platform only ever exposes SSH and Jupyter publicly, by design (Master
Spec §21) — a security boundary the project deliberately keeps narrow rather
than widening per-template. If you're running TensorBoard, a Gradio demo, or
anything else with its own port, `tunnel` reuses the SSH access you already
have instead of opening a new port on the instance:

```bash
ai-sandbox tunnel <instance-id> --port 6006   # TensorBoard, say
# now open http://localhost:6006
```

No provider or platform change, no new attack surface — this is the same
`ssh -L` port-forwarding pattern you'd use manually, just wrapped for
convenience.

## Auth refresh

You never need to re-run `login` just because time has passed. Access
tokens are short-lived by design; the CLI (via `ai_sandbox.Client`)
transparently exchanges the stored refresh token for a new access token on
the first 401 it sees and retries the request once, with no visible
interruption. This matters most for `launch`, which can poll for minutes
while a sandbox provisions. Only a *second* 401 — meaning the refresh token
itself is gone, e.g. after `logout` or on a machine that's been idle long
enough for it to expire — surfaces as `Not logged in. Run \`ai-sandbox
login\` first.`. See the SDK README's "Token refresh" section for the full
mechanism.

## Error handling

Every command exits non-zero on failure and prints a one-line message to
stderr rather than a raw traceback. The pattern throughout `main.py` is to
catch the specific `ai_sandbox` exception types that call for different
wording (`NotAuthenticatedError`, `DeviceLoginDeniedError`,
`DeviceLoginTimeoutError`, `FreeTextReviewQueuedError`) and fall back to the
`AiSandboxError` base class for everything else. `launch` additionally
treats `FreeTextReviewQueuedError` as a non-error: no instance was created,
but the request was queued for admin review, so it prints the review queue
id and exits 0. See `ai_sandbox`'s README for the full exception hierarchy
these messages are built from.
