Metadata-Version: 2.5
Name: trampoline-delta
Version: 0.2.0
Summary: Command line interface for the Delta service
Requires-Python: >=3.13
Requires-Dist: avalanche-ai==0.7.2
Requires-Dist: boto3<1.43.1,>=1.42.24
Requires-Dist: botocore<1.43.1,>=1.42.24
Requires-Dist: cryptography>=48.0.1
Requires-Dist: google-auth>=2.38.0
Requires-Dist: google-cloud-storage<4.0.0,>=3.13.0
Requires-Dist: httpx<1.0,>=0.27.0
Requires-Dist: jsonschema==4.26.0
Requires-Dist: pydantic>=2.13.4
Requires-Dist: python-ulid>=3.1.0
Requires-Dist: urllib3<3.0.0,>=2.7.0
Requires-Dist: zstandard>=0.23.0
Description-Content-Type: text/markdown

# Delta CLI

Delta CLI is a command line interface for the Delta service.

This repository contains the Delta command router, version output, and
browser-based CLI authentication.

## Features

- Built-in help via `delta --help` and per-command `--help`
- Version output via `delta version` and `delta --version`
- OAuth login with Authorization Code + PKCE
- One-shot `127.0.0.1:8400` callback server or an optional Console callback handoff
- Session storage in `~/.trampoline-ai/delta/auth.json` with owner-only permissions
- Automatic bearer authentication for service API commands

## Durable runs (`/v1`)

`delta durable-runs` consumes the committed merged OpenAPI contract. The legacy
`delta runs` group targeted the retired `/v2` lifecycle API and was removed in
0.2.0. Old invocations print a migration pointer to stderr and exit 2 without
network, configuration or authentication access; the stub is hidden from help.
Use `delta durable-runs --help` for the replacement commands.

~~~bash
delta durable-runs create --org ORG --project PROJECT --sid proof-1 \
  --fixture mixed --stages 1 --root-snapshot 'wobj_<64-lowercase-hex>'
delta durable-runs get RUN --org ORG --project PROJECT --json
delta durable-runs outputs RUN --org ORG --project PROJECT
delta durable-runs artifacts RUN --org ORG --project PROJECT
delta durable-runs download RUN INV reports/result.txt --dest ./result.txt \
  --org ORG --project PROJECT
delta durable-runs cancel RUN --cmd-id retry-stable-id --org ORG --project PROJECT
delta durable-runs events RUN --last-event-id 12 --max-reconnects 3 \
  --org ORG --project PROJECT --json
~~~

All commands use existing `delta login`, `DELTA_TOKEN`/`--token`, local project/user
config and `DELTA_ORG`/`DELTA_PROJECT` resolution. Explicit org/project flags work
outside a local checkout. The default API host is
`https://api.avalanche.run`, the authenticated `/v1` release host. Until it is
bound to the `/v1` gateway, point commands at `https://v2-gateway-staging.avalanche.run`
with `--api-url` or `DELTA_API_URL`.
API URL precedence (highest first): `--api-url`, `DELTA_API_URL`, project
`delta.toml` `api_url`, user `~/.config/trampoline-ai/delta/config.toml` `api_url`,
then the default.
`--api-url` accepts an origin or its `/v1` base. `--json`
prints generated model JSON without filling absent fields; events are JSON lines.
Wait fixtures require both `--wait-deadline-s` and `--wait-timer-s` (timer < deadline),
with no `--slow` and empty `--faults-json` (default `{}`).

Create through the selected edge project establishes the run binding. Reads of a
run not bound to that project report `404 run_not_found`, including cancel/events
before the rollout gate. Outputs/artifacts/download are live. Until runtime R16-d,
cancel/events report `503 durable_runs_upstream_unavailable` as **not yet available
on this deployment**, with exit status 1. Other errors preserve the edge `{detail}`
and runtime/new-route `{reason}` families.

Cancel prints its supplied or once-generated `cmd_id` to stderr, does not retry,
and tells you to retry with the **SAME `--cmd-id`** on ambiguous outcome 503. A 202
records the command; it does not prove child termination. SSE ignores heartbeat
comments, checks contiguous fact IDs, resumes exclusively after the last delivered
ID, and stops on terminal/stream_end/stream_error. Reconnects are bounded by a total
count (default 3); stream_error exits 1. Requests have 15-second HTTPX budgets;
SSE reads allow 60 seconds, above the 45-second upstream idle bound.

Download gets a JSON descriptor from the edge, then uses its exact signed URL and
headers directly against storage, without the edge bearer. Bytes must match the
immutable file size and SHA-256 before exclusive atomic publication. Existing
files/symlinks and racing destination creators are never overwritten. Destination
parents must exist. JSON download output is the descriptor and can contain a signed
URL: treat that output as sensitive.

### Generation and opt-in evidence

From the monorepo root:

~~~bash
uv run --frozen --package trampoline-delta python apps/cli/tools/generate_v1_client.py --write
uv run --frozen --package trampoline-delta python apps/cli/tools/generate_v1_client.py --check
uv run --frozen python apps/cli/tools/e2e_durable_run.py --help
~~~

`datamodel-code-generator==0.83.0` generates the reachable component closure for
Pydantic v2/Python 3.13. Narrow, schema-derived post-processing preserves conditional
and composition constraints with Draft 2020-12 entrypoint validators, PEP 695
aliases for hashable dictionary keys, Pydantic recursive JsonValue, compiled regex
patterns, and unchanged URI/date-time strings. No DTO fields are handwritten.
`--check` regenerates in memory and verifies frozen/merged/overlay hashes against
spec bytes, info fields and `apps/api/openapi/SHA256SUMS`. Builds verify committed
module digests and spec identities without requiring codegen at wheel runtime;
the generated models, constants and digest inventory ship in the wheel.

The E2E driver requires `--base`, `--org`, `--project`, `--run`, `--wrong-project`
and `--evidence`; supply `--binding`/`--path` together or auto-select the first file.
It uses only `DELTA_TOKEN`, the generated models and the CLI client, compares
outputs at equal watermarks, verifies direct-storage bytes, and requires the same
run to return `404 run_not_found` under the wrong project for all six exact routes.
Default evidence expects gated cancel/events. `--runtime-enabled` mutates the
selected owned fixture by cancelling it and observes incremental SSE (30-second /
20-frame smoke bound). Evidence includes candidate identity, all three spec hashes,
rerun arguments, status observations and cleanup, without payloads, bearer tokens,
signed URLs/query strings or headers. **Do not run against a live host without
explicit fixture authorization.**

## Requirements

- Python 3.13+
- uv

## Installation

From this directory, install the CLI in editable mode:

~~~bash
uv tool install -e .
~~~

You can also run the CLI directly during development:

~~~bash
uv run --with-editable . delta --help
uv run --with-editable . delta version
~~~

## Authentication Setup

The CLI uses a public OAuth application. Do not use secret keys for interactive
CLI user login.

The local session is stored in `~/.trampoline-ai/delta/auth.json`. Delta creates the
containing directory with mode `0700` and writes the credential file atomically with
mode `0600`.

Create an OAuth application in the same auth project as `../console`:

- Default redirect URI: `http://127.0.0.1:8400/callback`
- Console callback URI for `--manual-callback`:
  `https://cloud.avalanche.run/oauth/callback`
- Public client: enabled
- PKCE required: enabled
- Scopes: `profile email offline_access user:org:read`

A Clerk administrator must register the applicable redirect URI before its login mode
can succeed.

The production issuer, public OAuth client ID, and callback port are built in.
Sign in without configuration:

~~~bash
delta login
~~~

For another deployment, override the public auth configuration explicitly:

~~~bash
export DELTA_AUTH_ISSUER_URL="https://your-auth-issuer.example.com"
export DELTA_AUTH_CLIENT_ID="your_oauth_client_id"
~~~

For local development in this monorepo, the CLI can also derive the issuer from
`DELTA_AUTH_PUBLISHABLE_KEY`.

## Command Overview

### `delta version`

Shows the installed Delta CLI version.

### `delta auth login`

Signs in to Delta through your browser.

~~~bash
delta login
~~~

The CLI uses the registered callback port `8400`. Pass `--callback-port 0` for a
random loopback port when another provider supports RFC 8252 loopback redirects.

Use `--no-browser` to print the sign-in URL instead of opening it:

~~~bash
delta auth login --no-browser
~~~

Use the Console callback when the browser cannot reach the local loopback listener:

~~~bash
delta auth login --manual-callback
~~~

Manual callback sign-in requires Delta CLI 0.1.6 or newer. Older CLI releases cannot
complete this handoff; upgrade the CLI and start sign-in again if the Console reports
that an upgrade is required.

Complete sign-in in the browser, select **Copy response** at the Console callback
page, then paste only that response into Delta's hidden terminal prompt. Never copy,
paste, log, or share the raw browser callback URL. The response is bound to the
in-memory PKCE sign-in attempt; do not paste it into logs or chat.

### `delta auth status`

Shows whether you are signed in.

### `delta auth whoami`

Shows the signed-in account.

### `delta auth logout`

Signs out of Delta.

### `delta --help`

Shows available commands and global options.

Service API commands automatically load and refresh the signed-in session from
`~/.trampoline-ai/delta/auth.json`. An explicit `--token` wins, then a nonblank
`DELTA_TOKEN`, then the saved session; the user TOML configuration rejects plaintext
tokens.

The API enforces the selected Clerk organization and its role permissions. The
signed-in user must belong to the requested organization, and that role must
include the Delta action used by the service command.

## Listing pages

Collection commands accept `--limit` (default 100, maximum 500) and `--cursor`.
Human output prints the next continuation; list JSON is now an
`{items,next_cursor}` envelope. `HttpDeploymentApi` returns a list-compatible
`ApiPage` exposing `next_cursor`; iterating it makes no additional requests.

Live paging continues after the last returned item. Refresh without a cursor to
see newer items that sort before that position. Filtered pages may contain no
items and still have a continuation.

Webhook URLs now include organization, project and flow scope:
`/v2/hooks/orgs/ORG/projects/PROJECT/flows/FLOW/[CONFIGURED_PATH/]TOKEN`.
Use the complete URL returned by the API; opaque-only URLs are removed.
