Metadata-Version: 2.5
Name: gravity-cli
Version: 0.5.3
Summary: gravity cli — init, run, and push python agents to the Gravity marketplace
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Requires-Python: >=3.12
Requires-Dist: gravity-schema==0.5.3
Requires-Dist: httpx>=0.27.0
Requires-Dist: keyring>=25.0.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: rich>=13.9.0
Requires-Dist: typer>=0.15.0
Requires-Dist: uv>=0.8.0
Description-Content-Type: text/markdown

<h1 align="center">Gravity CLI</h1>

<p align="center">
  <b>The command-line tool for building, testing and publishing AI agents on Gravity.</b>
  <br>
  Write ordinary LangGraph in Python, declare what your agent may touch in one manifest,
  <br>
  and run it against real tools with exactly those permissions enforced.
</p>

<p align="center">
  <a href="https://pypi.org/project/gravity-cli/"><img alt="PyPI" src="https://img.shields.io/pypi/v/gravity-cli?style=flat-square&color=ff6b35&label=pypi"></a>
  <a href="https://www.python.org/downloads/"><img alt="Python 3.12+" src="https://img.shields.io/badge/python-3.12%2B-ffb627?style=flat-square"></a>
  <a href="https://www.apache.org/licenses/LICENSE-2.0"><img alt="License: Apache 2.0" src="https://img.shields.io/badge/license-Apache%202.0-e63946?style=flat-square"></a>
  <img alt="1,139 tools in the catalog" src="https://img.shields.io/badge/catalog-1%2C139%20tools-8338ec?style=flat-square">
  <img alt="4 templates" src="https://img.shields.io/badge/templates-4-3a86ff?style=flat-square">
  <a href="https://docs.astral.sh/ruff/"><img alt="Linted with Ruff" src="https://img.shields.io/badge/lint-ruff-261230?style=flat-square"></a>
</p>

<p align="center">
  <a href="#installation">Install</a> ·
  <a href="#quick-start">Quick start</a> ·
  <a href="#command-reference">Commands</a> ·
  <a href="#configuration">Configuration</a> ·
  <a href="#the-manifest">Manifest</a> ·
  <a href="#troubleshooting">Troubleshooting</a>
</p>

---

`gravity run` on an agent that called a tool it never declared. The gateway refused that one call, and the run carried on:

```
  inbox-digest@1.0.0  run grt_…4e7b · 2 tools declared

  ● read                                                          0.9s
    ✓ gmail.read              3 found                             0.8s
  ● digest                                                        2.3s
    ◆ openai/gpt-6-luna                                           2.1s
    ✗ slack.send              refused — not in permissions.tools  0.0s

  ╭─ result ───────────────────────────────────────────────────╮
  │ unread    3                                                │
  │ headline  Two invoices and a meeting request need replies. │
  ╰────────────────────────────────────────────────────────────╯

  ✓ Done in 3.3s
```

```bash
pipx install gravity-cli
gravity init my-agent
```

> **Status.** `login`, `push`, `run` and the tool catalog work out of the box against Gravity's hosted services. `gravity login` issues you the gateway token that `run` needs; nothing goes in `.env`.

## Contents

- [Declare it once, enforced on every call](#declare-it-once-enforced-on-every-call)
- [What is in the box](#what-is-in-the-box)
- [Five things to know before you start](#five-things-to-know-before-you-start)
- [Installation](#installation)
- [Quick start](#quick-start)
- [How it fits together](#how-it-fits-together)
- [Command reference](#command-reference)
- [Configuration](#configuration)
- [Authentication](#authentication)
- [The manifest](#the-manifest)
- [An agent project](#an-agent-project)
- [Running a local gateway](#running-a-local-gateway)
- [Troubleshooting](#troubleshooting)
- [Limitations](#limitations)
- [Development](#development)
- [Documentation](#documentation)

## Declare it once, enforced on every call

There is no SDK to learn. Your agent is plain LangGraph that reads three environment variables. Everything Gravity enforces comes from `manifest.yaml`:

| You declare in `manifest.yaml` | What Gravity does with it |
|---|---|
| `inputs` | Renders the form a consumer fills in, and passes the answers in `state["inputs"]` |
| `permissions.tools` | Issues a run token that allows exactly those tools. Any other call is refused by the gateway |
| `approvals.required_for` | Pauses those calls until a person approves them |
| `triggers.schedule` | Runs the agent on that cron schedule |
| `state` | Keeps a small memory between runs |

The same rules apply on your laptop and when the agent runs hosted, because both go through the same gateway.

## What is in the box

| | |
|---|---|
| Commands | 16, grouped into build, publish, inspect, account and help |
| Starter templates | 4: `minimal`, `structured`, `deepagent`, `chat` |
| Tool catalog | 1,139 tools across 8 apps (Gmail, Slack, Linear, GitHub, Calendar, Drive, Sheets, LinkedIn), 25 of them curated |
| Test suite | 285 tests, run on Ubuntu and Windows with Python 3.12 and 3.13 |
| Runtime contract | 3 environment variables, no Gravity import in your code |

## Five things to know before you start

- Inputs arrive in `state["inputs"]`; whatever you put in `state["result"]` is what the consumer gets.
- `graph` is an `async def` factory. Read environment variables inside it, never at import time.
- A tool that is not in `permissions.tools` is refused, whatever your code does.
- A tool listed in `approvals.required_for` waits for a yes in the terminal before it runs.
- The platform runs Python 3.12. `gravity init` writes that, and `gravity validate` fails on anything else.

## Installation

Requires Python 3.12 or newer. Agents use [uv](https://docs.astral.sh/uv/) for their own environments.

```bash
pipx install gravity-cli       # recommended: gravity gets its own environment
uv tool install gravity-cli    # the same, with uv
pip install gravity-cli        # into the current environment
```

`gravity` then works from any folder. To upgrade: `pipx upgrade gravity-cli` or `uv tool upgrade gravity-cli`.

If `pip` answers `No matching distribution found for gravity-cli`, your Python is older than 3.12 (check with `python --version`; conda's `base` often is). `uv tool install gravity-cli` avoids the problem, because uv fetches a suitable Python by itself. With pip, pick a newer interpreter: `py -3.12 -m pip install gravity-cli` on Windows, or `python3.12 -m pip install gravity-cli` elsewhere.

The first time you run `gravity`, it shows a short welcome and offers to create your first agent.

## Quick start

**1. Create and check an agent.** No account or setup needed:

```bash
gravity init my-agent
cd my-agent
uv venv && uv pip install -r requirements.txt
gravity validate
```

**2. Run it.** Needs a gateway and your dev token, see [Configuration](#configuration):

```bash
gravity run --input query="hello"
```

**3. Publish it.** Needs a Gravity account:

```bash
gravity build
gravity login
gravity push
```

## How it fits together

```
 your machine                                        Gravity
 ────────────                                        ───────
 gravity init / validate / build                     (nothing leaves your machine)

 gravity run ────── dev token ──────────────────→   gateway ──→ tools (MCP) and models (/v1)
    └─ starts your agent with a run token ──────→   gateway     only the declared tools are allowed

 gravity login / push / tools / agents ─────────→   control plane ──→ builds and hosted runs
```

| Service | What it does | Used by |
|---|---|---|
| **Gateway** | Serves tools over MCP (`/mcp`) and models over an OpenAI-compatible API (`/v1`). Lists its tools at `/tools` and your connected accounts at `/connections`. Issues run tokens and refuses any tool a run was not granted. | `run`, `schedule`, `adapt`, `tools`, `connect`, and your agent |
| **Control plane** | Accounts, pushed agents, builds and hosted runs. | `login`, `logout`, `whoami`, `push`, `agents`, `runs`, `logs` |

On `gravity run`, the CLI trades your dev token for a run token scoped to the manifest's tools and starts the agent with only that token. Your own tokens are removed from the agent's environment, so agent code can never act as you.

<br>

---

## Command reference

Run `gravity help` for this list in the terminal, and add `-h` to any command for its options.

| Group | Command | What it does |
|---|---|---|
| **Build an agent** | [`gravity init`](#gravity-init) | Create a new agent from a template |
| | [`gravity validate`](#gravity-validate) | Check your agent for mistakes before it runs |
| | [`gravity run`](#gravity-run) | Run your agent locally against real tools |
| | [`gravity dev`](#gravity-dev) | Open a local dashboard to run and watch your agent |
| | [`gravity schedule`](#gravity-schedule) | Run your agent on its schedule, locally |
| | [`gravity adapt`](#gravity-adapt) | Write a new slot adapter with AI, kept only if its tests pass |
| **Publish** | [`gravity build`](#gravity-build) | Package your agent into `build/agent.zip` |
| | [`gravity push`](#gravity-push) | Publish your built agent to Gravity |
| **Your agents** | [`gravity agents`](#gravity-agents) | List your published agents and their review status |
| | [`gravity runs`](#gravity-runs) | Show recent runs of one agent |
| | [`gravity logs`](#gravity-logs) | Show the logs of one run |
| | [`gravity tools`](#gravity-tools) | Browse the tools your agent can use |
| **Account** | [`gravity login`](#gravity-login) | Sign in to Gravity in your browser |
| | [`gravity logout`](#gravity-logout) | Sign out |
| | [`gravity whoami`](#gravity-whoami) | Show who you are signed in as |
| | [`gravity connect`](#gravity-connect) | Connect an account your agents act on (Gmail, Slack...) |
| **Help** | [`gravity guide`](#gravity-guide) | Read the builder guide: how to build a good agent |
| | [`gravity help`](#gravity-help) | Show the list of commands |

`gravity --api-url URL <command>` saves a different control-plane address to `~/.gravity/config.json`. Wherever a command takes `[path]`, it is the agent's folder and defaults to the current one.

### `gravity init`

Create a new agent that already runs: a filled-in manifest, pinned dependencies and a working `src/agent.py`.

```
gravity init [name] [--template TEMPLATE] [--here]
```

| Argument or option | Description | Default |
|---|---|---|
| `name` | Name of the agent, and of the folder created. Lowercase letters, digits and single hyphens. | required, unless `--here` |
| `--template` | Starting point: `minimal`, `structured`, `deepagent` or `chat` | `minimal` |
| `--here` | Add only a `manifest.yaml` to the current folder, for an existing project. The name comes from the folder. | off |

| Template | What you get |
|---|---|
| `minimal` | One file. A LangGraph agent that answers a query with the declared tools |
| `structured` | The same agent split into state, prompts, nodes and gateway modules, for larger projects |
| `deepagent` | An agent built on the `deepagents` harness, with file access denied by default |
| `chat` | A conversational agent that keeps the thread between turns |

```bash
gravity init inbox-digest
gravity init support-bot --template chat
gravity init --here
```

It refuses to overwrite an existing folder or `manifest.yaml`.

### `gravity validate`

Check an agent on your machine, with no Gravity service involved. It reports every problem at once, each with a suggested fix.

```
gravity validate [path]
```

What it checks:

- `manifest.yaml` is valid, field by field
- the entrypoint file exists and defines the object the manifest names
- `requirements.txt` exists, and compiles into `requirements.lock` for the platform image (ARM64 Linux) and the manifest's Python version
- every adapter a slot declares has a file, and every adapter file is declared
- every tool in the manifest exists in the catalog, the same list the platform's build uses, so a typo fails here instead of after a push
- every imported write tool is under `approvals.required_for`
- your code never names a write tool the manifest does not grant
- a warning, never a failure, for read tools your code names but the manifest does not declare

```bash
gravity validate
gravity validate ./my-agent
```

Exits with code 1 if anything is wrong.

### `gravity run`

Run the agent locally through the gateway and watch it live: each step, model call and tool call, then a result card.

```
gravity run [path] [--input KEY=VALUE]... [--reply TEXT] [--llm-url URL]
```

| Option | Description |
|---|---|
| `--input KEY=VALUE` | An input, repeatable. `KEY=@file.txt` reads the value from a file. Inputs with a `default` in the manifest can be left out. |
| `--reply TEXT` | Continue the last conversation of a chat agent |
| `--llm-url URL` | Model endpoint to use instead of `GRAVITY_LLM_URL` |

```bash
gravity run --input query="summarise my unread email"
gravity run --input transcript=@call.txt --input tone=formal
gravity run --reply "make it shorter"
```

- Requires a gateway token: the one `gravity login` stores, or `GRAVITY_DEV_TOKEN`.
- A call to a tool in `approvals.required_for` pauses, shows what it would do, and waits for `y` or `n`. With no terminal to ask in, it is rejected.
- Exits with code 0 when the run finishes and 1 when it fails.
- Leaves a record in `.gravity/runs/<id>/` (`run.json` and `events.jsonl`), which is what `gravity dev` reads. One run is live per folder at a time, because runs share the folder's memory, store and thread files.

### `gravity dev`

Open a local dashboard for the agent in this folder: a form built from the manifest's inputs, a live timeline of each step, model call and tool call, approval buttons for held calls, the result, and a history of runs.

```
gravity dev [path] [--port 4747] [--no-open] [--llm-url URL]
```

| Option | Description |
|---|---|
| `--port N` | Port to listen on (default 4747). If it is taken, a free one is used and printed. |
| `--no-open` | Print the address without opening a browser (SSH, WSL, CI) |
| `--llm-url URL` | Model endpoint to use instead of `GRAVITY_LLM_URL` |

- A run from `gravity run` in the same folder shows up in the dashboard live, and a run started in the dashboard is the same run (same engine, same memory, store and thread files), so `gravity run --reply` continues it.
- It listens on `127.0.0.1` only, behind a random token in the printed address, for as long as the command runs. Ctrl+C stops it and any run it started. Nothing keeps running in the background.
- Connected accounts: it shows the accounts your tools need and a Connect button for each one that is missing.
- Approvals: a call held for approval can be decided in the terminal or in the dashboard; whichever answers first wins.
- Cancel a run from the dashboard only if the dashboard started it; stop a terminal run with Ctrl+C in that terminal.
- Results show as text. Word, PowerPoint and CSV files are produced by Gravity after you push. A file input is read as text here.
- Same requirements as `gravity run`: a gateway token from `gravity login`, or `GRAVITY_DEV_TOKEN`.

### `gravity schedule`

Run the agent on its `triggers.schedule`, the way the platform will, until you press Ctrl+C. Each tick is an ordinary run with the same inputs.

```
gravity schedule [path] [--input KEY=VALUE]... [--cron EXPR] [--now]
```

| Option | Description |
|---|---|
| `--input KEY=VALUE` | An input, repeatable. The same inputs are used for every run. |
| `--cron EXPR` | Use this 5-field cron schedule instead of the manifest's, if the manifest allows changes |
| `--now` | Run once right away, then keep to the schedule |

```bash
gravity schedule --input send_to=me@example.com
gravity schedule --cron "*/30 * * * *" --now
```

Nobody is at the terminal during a scheduled run, so calls that need approval are rejected and the agent takes its fallback. A failed run does not stop the schedule.

### `gravity adapt`

Have a coding agent write one new adapter for a slot. The file is kept only if the slot's conformance tests pass; otherwise the file and the manifest are put back as they were.

```
gravity adapt <slot> <name> --tools TOOLS [--hint TEXT] [--model MODEL] [--path PATH]
```

| Argument or option | Description | Default |
|---|---|---|
| `slot` | The slot to extend, for example `deliver` | required |
| `name` | Name of the new adapter, for example `slack` | required |
| `--tools` | Tools the adapter may call, comma-separated. This is what it is granted. | required |
| `--hint` | Anything the coding agent should know, for example `'post to #leads'` | none |
| `--model` | Model that writes the adapter | `anthropic/claude-sonnet-5` |
| `--path` | Agent folder | current folder |

```bash
gravity adapt deliver slack --tools slack.send,slack.list_channels --hint "post to #leads"
```

Requires `tests/test_<slot>_adapters.py` in the project, `GRAVITY_DEV_TOKEN` and a reachable gateway. It makes up to 3 attempts, feeding each test failure back to the model.

### `gravity build`

Validate the agent, then bundle it into `build/agent.zip` with its hashes in `build/build.json`. Nothing leaves your machine, so you can inspect the zip before publishing.

```
gravity build [path]
```

The bundle holds `manifest.yaml`, `requirements.txt`, `requirements.lock`, `src/` and `tests/`. The same source always produces the same bytes.

```bash
gravity build
```

### `gravity push`

Upload exactly what `gravity build` made, then wait while Gravity builds it.

```
gravity push [path]
```

- Requires `gravity login`, or `GRAVITY_API_TOKEN` for development.
- Refuses if there is no build, or if the source changed since the build. Run `gravity build` again.
- A published version can never be replaced. Raise `version` in the manifest to publish again.
- Press Ctrl+C to stop waiting; the build carries on, and `gravity agents` shows its status.

```bash
gravity build && gravity push
```

### `gravity agents`

List your published agents with their version, status and last update.

```
gravity agents
```

### `gravity runs`

Show recent hosted runs of one agent, with status, start time and cost.

```
gravity runs <agent_name>
```

```bash
gravity runs inbox-digest
```

### `gravity logs`

Show the logs of one hosted run. The run id comes from `gravity runs`.

```
gravity logs <run_id>
```

### `gravity tools`

Browse the tool catalog: the names you can put under `permissions.tools`, with what each one does. It is the gateway's own list, so it shows exactly what a run can call, and it needs no sign-in.

About 25 tools are curated, such as `gmail.read` and `web.search`. The other ~1,100 are imported from the same apps and unreviewed: any imported tool that changes something must also be under `approvals.required_for`, and `gravity validate` checks that.

```
gravity tools [search]
```

```bash
gravity tools           # everything
gravity tools gmail     # only tools whose name contains "gmail"
```

### `gravity login`

Sign in to Gravity. It opens your browser, shows a short code to confirm, and stores the session in your operating system's keychain.

```
gravity login
```

### `gravity logout`

Remove the stored session.

```
gravity logout
```

### `gravity whoami`

Show the account you are signed in as, and whether you hold a gateway token and until when.

```
gravity whoami
gravity whoami --token    # print the gateway token itself, for CI or a teammate's .env
```

### `gravity connect`

Connect an account your agents' tools act on, through the app's own sign-in page in your browser. With no app, list what is connected.

```
gravity connect [app]
```

```bash
gravity connect           # what is connected, and whose accounts runs act on
gravity connect gmail     # opens the sign-in page, then waits until it is done
gravity connect sheets    # use the app name from tool names: sheets, calendar, drive, linear, slack, github...
```

- Uses the gateway token from `gravity login`.
- `web` tools need no account.

### `gravity guide`

Read the builder guide, which ships with the CLI: how an agent is put together, the rules your code must follow, formatting results, approvals, adapters, scheduling, memory, and a checklist.

```
gravity guide [--save]
```

| Option | Description |
|---|---|
| `--save` | Write `BUILDER_GUIDE.md` to the current folder instead of showing it |

On a terminal it asks which section you want. Piped, it prints the whole guide.

```bash
gravity guide
gravity guide --save
gravity guide | less
```

### `gravity help`

Show the list of commands. The same as `gravity -h`.

```
gravity help
```

<br>

---

## Configuration

### Where settings live

Put them in `~/.gravity/.env` (on Windows, `%USERPROFILE%\.gravity\.env`). Every `gravity` command loads this file, from any folder, so you set a value once. With Gravity's hosted services, `gravity login` is all you need, and the file can stay empty.

```bash
# ~/.gravity/.env
GRAVITY_DEV_TOKEN=<a gateway token, only if gravity login did not issue you one>
```

That is the only setting most builders ever need. The CLI knows where Gravity's control plane is, and asks it where the gateway is.

When a setting is given in more than one place, the first match wins:

1. a variable exported in your shell
2. `~/.gravity/.env`
3. `~/.gravity/config.json` (only the API address, written by `gravity --api-url URL <command>`)
4. the built-in default

Never commit a `.env` file. The `.gitignore` in every template keeps `.env` and `.gravity/` out of git.

### What each task needs

| You want to | Commands | Required | Usually also set |
|---|---|---|---|
| Create, check and package an agent | `init`, `validate`, `build` | nothing | nothing |
| Run an agent locally | `run`, `schedule`, `adapt` | `gravity login` (or `GRAVITY_DEV_TOKEN`) | nothing |
| Publish and inspect agents | `push`, `agents`, `runs`, `logs`, `whoami` | `gravity login` | nothing |
| Browse tools, connect accounts | `tools`, `connect` | nothing for `tools`; `gravity login` for `connect` | nothing |

Set `GRAVITY_API_URL` and `GRAVITY_GATEWAY_URL` only to work against services you run yourself; see [Running a local gateway](#running-a-local-gateway).

### Environment variables

| Variable | What it is | Default |
|---|---|---|
| `GRAVITY_DEV_TOKEN` | A gateway token. `gravity run` trades it for a run token scoped to the manifest's tools. Overrides the token `gravity login` stored; needed only when login did not issue one, such as against a local gateway. | the token from `gravity login` |
| `GRAVITY_GATEWAY_URL` | The gateway's MCP endpoint, ending in `/mcp` | the gateway the control plane names in `GET /api/cli/config` |
| `GRAVITY_LLM_URL` | The OpenAI-compatible model endpoint | the gateway's address + `/v1` |
| `GRAVITY_API_URL` | The control plane's address | `https://agents.dev.gravity.fast` |
| `GRAVITY_API_TOKEN` | A control-plane dev token, used instead of `gravity login`. For development only. | none |
| `GRAVITY_CONFIG_DIR` | Moves `~/.gravity` elsewhere. Useful for tests and CI. | `~/.gravity` |
| `GRAVITY_UNATTENDED` | When `1`, calls that need approval are rejected without asking. `gravity schedule` sets it for you. | unset |

### What your agent receives

`gravity run` starts your agent with exactly these three, and the hosted runner does the same. You never set them yourself:

| Variable | Value |
|---|---|
| `GRAVITY_GATEWAY_URL` | the gateway's `/mcp` endpoint, for tools |
| `GRAVITY_LLM_URL` | the gateway's `/v1` endpoint, for models |
| `GRAVITY_RUN_TOKEN` | a token for this run only. Use it as the bearer token for tools and as the API key for models. |

## Authentication

Two separate credentials, for two separate services:

| Credential | Used by | How you get it |
|---|---|---|
| Control-plane session | `push`, `tools`, `agents`, `runs`, `logs`, `whoami` | `gravity login` |
| Gateway token | `run`, `schedule`, `adapt` | `gravity login` asks Gravity for one and stores it with the session. `gravity whoami` shows its expiry; `gravity whoami --token` prints it. `GRAVITY_DEV_TOKEN`, if set, is used instead. |

`gravity login` stores the session and gateway token in the operating system's keychain. On machines without one, it falls back to `~/.gravity/session.json`, readable only by you. `gravity logout` removes it.

For development, setting `GRAVITY_API_TOKEN` to the control plane's dev token skips login. `gravity whoami` then prints the dev user's id followed by `(dev token)`.

<br>

---

## The manifest

`manifest.yaml` declares everything about the agent that Gravity enforces. Only `name` and `runtime.entrypoint` are required; everything else has a default.

The smallest useful manifest:

```yaml
contract: v1
name: my-agent
version: 1.0.0
runtime:
  entrypoint: src/agent.py:graph
inputs:
  - name: query
    type: text
    label: "What should the agent do?"
    required: true
permissions:
  tools: [example.echo]
```

<details>
<summary><b>A complete manifest, with every section annotated</b></summary>

<br>

This one is valid as written.

```yaml
contract: v1                      # manifest contract version; v1 is the only one
name: inbox-digest                # lowercase letters, digits, single hyphens; appears in URLs
version: 1.0.0                    # semver; a published version is frozen, so bump it to publish again
description: "Summarises unread email and sends a digest"
kind: interactive                 # transform | interactive | monitor

runtime:
  framework: langgraph            # the only framework in v1
  python: "3.12"                  # the platform image runs Python 3.12
  entrypoint: src/agent.py:graph  # file:object, relative to the project folder
  timeout_seconds: 300            # 10 to 28800

dependencies:
  lockfile: requirements.lock     # written by `gravity validate`

inputs:                           # each becomes a field in the consumer's form
  - name: lookback
    type: static-dropdown
    label: "How far back?"
    options: ["1 day", "7 days"]
    default: "1 day"
  - name: send_to
    type: email
    label: "Send the digest to"
    required: true

permissions:
  tools:                          # the only tools this agent's runs may call
    - gmail.read
    - gmail.send
  models:
    tier: standard                # standard | premium

approvals:
  required_for:                   # these calls pause until a person approves them
    - gmail.send

resources:
  memory_mb: 512                  # 128 to 4096; a request the platform may lower

triggers:
  manual: true                    # the consumer can start a run
  schedule:
    cron: "0 8 * * 1-5"           # weekdays at 08:00, local time
    consumer_can_change: true

state:
  max_kb: 64                      # memory kept between runs (1 to 256 KB); omit for none

slots:                            # a part the consumer chooses; each adapter gets only its own tools
  deliver:
    label: "Where should the digest go?"
    default: email
    adapters:
      email: [gmail.draft]        # code in src/adapters/deliver/email.py
      slack: [slack.send]         # code in src/adapters/deliver/slack.py

outputs:                          # extra formats the platform renders and delivers itself
  report:
    renderer: markdown            # markdown | docx | pptx | rows
    label: "Digest as a document"
    destinations: [download]
```

</details>

### Field reference

| Field | Required | Default | Rules |
|---|---|---|---|
| `contract` | no | `v1` | Only `v1` exists. |
| `name` | **yes** | | Lowercase letters, digits and single hyphens, up to 64 characters. Appears in URLs. |
| `version` | no | `1.0.0` | Strict semver, such as `1.2.0`. A published version can never change. |
| `description` | no | empty | Up to 500 characters. |
| `kind` | no | `interactive` | `transform` (input in, result out), `interactive` (calls tools mid-run, may converse) or `monitor` (runs on a schedule or webhook and remembers the last run; needs a trigger). |
| `runtime.entrypoint` | **yes** | | `path/to/file.py:object`, inside the project. The object is your `graph` factory. |
| `runtime.framework` | no | `langgraph` | Only `langgraph`. |
| `runtime.python` | no | `3.12` | What the platform image runs. `gravity validate` fails on any other value, because the hosted build would. |
| `runtime.timeout_seconds` | no | `300` | 10 to 28800. |
| `dependencies.lockfile` | no | `requirements.lock` | Compiled by `gravity validate`; ships with the agent. |
| `inputs[]` | no | none | Up to 50. Each has `name` (a lowercase identifier, the key in `state["inputs"]`), `type`, `label`, and optionally `required`, `default`, `options`, `accept`. |
| `inputs[].type` | **yes** | | `text`, `textarea`, `number`, `boolean`, `static-dropdown` (needs `options`), `email`, `url`, `date`, `secret` (an API key, password or other confidential value) or `file` (no `default`; `accept` lists extensions such as `[pdf, csv]`). |
| `permissions.tools` | no | none | Up to 50 catalog names, such as `gmail.read`. Run `gravity tools` for the list. |
| `permissions.models.tier` | no | `standard` | `standard` or `premium`. |
| `approvals.required_for` | no | none | Tools that pause until a person approves. Each must also be declared under `permissions.tools` or a slot adapter. |
| `resources.memory_mb` | no | `512` | 128 to 4096. A request; the platform may lower it. |
| `triggers.manual` | no | `true` | Whether the consumer can start a run. Something must start the agent: `manual`, a `schedule` or a `webhook`. |
| `triggers.schedule` | no | none | `cron` (5 fields, local time, such as `"0 9 * * 1"`) and `consumer_can_change` (default `true`). |
| `triggers.webhook` | no | none | A list of events; `github.push` is the only one today. |
| `state.max_kb` | no | no memory | Include `state:` to keep memory between runs, in `state["memory"]`. 1 to 256 KB. |
| `slots.<name>` | no | none | A swappable part: `label`, `default` adapter, and `adapters` mapping each adapter name to the tools it may call. Code lives in `src/adapters/<slot>/<adapter>.py`. Slot names cannot repeat an input's name. |
| `outputs.<name>` | no | none | Up to 8 extra formats rendered from the result: `renderer` (`markdown`, `docx`, `pptx` or `rows`), `label` and `destinations` (`download`, or a catalog tool such as `gmail.send`). |

Unknown fields are rejected rather than ignored, so a typo is reported instead of silently dropped.

## An agent project

```
my-agent/
├── manifest.yaml       what the agent is and may do
├── requirements.txt    your dependencies
├── requirements.lock   compiled by `gravity validate`; commit it
└── src/
    └── agent.py        exports `graph`, an async factory returning a compiled LangGraph graph
```

Files the CLI writes:

| Path | Written by | Contents |
|---|---|---|
| `requirements.lock` | `validate` | Exact dependency versions for the platform image |
| `build/agent.zip`, `build/build.json` | `build` | The bundle `push` uploads, and its hashes |
| `.gravity/payload.json` | `run` | The inputs passed to the agent |
| `.gravity/thread.json` | `run` | The last conversation of a chat agent, for `--reply` |
| `.gravity/memory.json` | `run` | Memory kept between runs, when the manifest declares `state` |

The template `.gitignore` excludes `build/`, `.gravity/` and `.env`.

## Running a local gateway

The gateway belongs to the Gravity platform repository and is run by the Gravity team. Builders outside the team use the hosted gateway instead.

<details>
<summary><b>Gateway settings, for the Gravity team</b></summary>

<br>

Start it with `gravity-gateway serve`. It listens on port 8000, prints a dev token, and reads its own `.env` in the gateway's folder. With nothing set, it serves only the built-in `example.echo` tool and keeps tokens in memory.

| Variable | What it enables | When you need it |
|---|---|---|
| `COMPOSIO_API_KEY` | Real tools (Gmail, Slack, Linear and others). Unset: only `example.echo`. | To call any real tool |
| `OPENROUTER_API_KEY` | Model calls on `/v1`. Unset: `/v1` returns 503. | For any agent that calls a model |
| `OPENROUTER_BASE_URL` | Where model calls go. Default `https://openrouter.ai/api/v1`. | Rarely |
| `LOCAL_DEV_TOKEN` | A fixed dev token (24+ random characters), so restarts don't change it. Use the same value as `GRAVITY_DEV_TOKEN`. | Recommended |
| `GATEWAY_PORT` | The listening port. Default `8000`. | Only if 8000 is taken |
| `DATABASE_URL`, `PLATFORM_DB_SCHEMA` | Keeps run tokens in the platform database instead of memory | Platform development |
| `PLATFORM_SERVICE_KEY`, `AWS_REGION` | Lets hosted runs mint tokens and report back | Platform development |

A fully local setup, in `~/.gravity/.env`:

```bash
GRAVITY_DEV_TOKEN=<the gateway's LOCAL_DEV_TOKEN>
GRAVITY_GATEWAY_URL=http://127.0.0.1:8000/mcp
GRAVITY_API_URL=http://localhost:3010
GRAVITY_API_TOKEN=<the control plane's PLATFORM_DEV_TOKEN>
```

Real tools act on a connected account. Connect one with `gravity-gateway connect --user-id builder:local --toolkit <app>`.

</details>

## Troubleshooting

| Message | Cause and fix |
|---|---|
| `No matching distribution found for gravity-cli` | pip is running on a Python older than 3.12. Use `uv tool install gravity-cli`, or see [Installation](#installation). |
| `No gateway token` | `run`, `schedule` and `adapt` need a gateway token. Run `gravity login`, or add `GRAVITY_DEV_TOKEN` to `~/.gravity/.env`. |
| `Your gateway token has expired` | The token `gravity login` stored has lapsed. Run `gravity login` again. |
| `Can't reach the gateway at …` | No gateway at that address. Set `GRAVITY_GATEWAY_URL` to your gateway's `/mcp` address. |
| `The gateway refused this run` | The gateway rejected the token or a declared tool. Run `gravity whoami` to check the token, or `gravity login` for a fresh one. |
| `You are not signed in` | A control-plane command without credentials. Run `gravity login`. |
| `Can't reach Gravity at …` | The control plane did not answer. Check `GRAVITY_API_URL` in `~/.gravity/.env`. |
| `Missing required input: …` | Pass each required input with `--input name=value`, or give it a `default` in the manifest. |
| `No build to push` or `The build is stale` | Run `gravity build` again after any change to `src/`, the manifest or the requirements. |
| `✗ requirements.lock` in `gravity validate` | A dependency has no prebuilt package for the platform image (ARM64 Linux). Pick a version that publishes one, or a different package. |
| A tool row shows `refused — not in permissions.tools` | The agent called a tool its manifest does not declare. Add it under `permissions.tools`, then run again. |

## What's new

- **0.5.3** `gravity dev` opens a local dashboard for your agent: a form from your manifest, a live timeline, approvals, the result and run history; runs from `gravity run` in the same folder show up in it live, and the other way round. WhatsApp joins the catalog (51 tools; 1,374 across 12 apps). Agents can return generated images: put them in a corpus section's `images` and they land in the Word, PowerPoint and Markdown outputs. The agent standard now shows how to generate images and embeddings through the gateway. `NEEDS_RECONNECT` on the shared test account tells you to ask Gravity, not to connect your own account.
- **0.5.2** Jira, Confluence and Notion join the catalog: 1,351 tools across 11 apps. `get_store()` works in local runs, kept in `.gravity/store.json`. Renewing an expired sign-in keeps your gateway token, so `gravity run` keeps working after an hour. Commands start faster: the catalog loads in a quarter of the time.
- **0.5.0** `gravity connect` links your own accounts. `web.search` and `web.fetch` join the catalog. `gravity validate` now refuses a tool that isn't in the catalog, an imported write tool without an approval, and code that names a write tool the manifest doesn't grant. `gravity tools` reads the gateway's list and needs no sign-in. On Windows the session stays in the keychain instead of a file.
- **0.4.4** Same code as 0.4.3, released from this repository so the GitHub release, the tag and PyPI line up again. Adds `RELEASING.md`.
- **0.4.3** Documents the `secret` input type and the `supadata.get_transcript` tool.
- **0.4.1** New input type `secret` for an API key, password or other confidential value. New built-in tool `supadata.get_transcript` (`gravity tools supadata`).

## Limitations

- **No offline mode.** `gravity run` always talks to a real gateway. There is no mock-tool mode.
- **Heuristic drift check.** `gravity validate` finds undeclared tools by scanning source for imports and tool-shaped strings. Expect some false positives; they are warnings, never failures.
- **Models are not restricted yet.** The gateway forwards whatever model name the agent asks for, and `permissions.models.tier` is not enforced.
- **Hosted history.** `gravity runs` and `gravity logs` show what the control plane returns, which is empty until an agent has hosted runs.

## Development

```bash
git clone https://github.com/AIGravity/gravity-cli.git
cd gravity-cli
uv sync                                                   # both packages and the dev tools
uv run gravity --help
uv run pytest                                             # CLI tests
cd packages/gravity-schema && uv run pytest && cd ../..   # manifest contract tests
uv run ruff check .
```

The repository holds two packages that are always released together at the same version:

| Package | What it is |
|---|---|
| `gravity-cli` | The `gravity` command (`src/gravity_cli/`) |
| `gravity-schema` | The manifest contract: models, validation, the tool catalog and its JSON Schema (`packages/gravity-schema/`). The platform's servers use it too, so a manifest is judged the same way everywhere. |

After changing the manifest model, regenerate the committed JSON Schema with `uv run python -m gravity_schema`. A test fails until you do.

<details>
<summary><b>Continuous integration and releasing</b></summary>

<br>

Every pull request and every push to `main` runs [`ci.yml`](.github/workflows/ci.yml):

- `ruff check`, and a check that `uv.lock` matches `pyproject.toml`
- both test suites on Ubuntu and Windows, with Python 3.12 and 3.13
- a build of both packages, installed into a clean environment and used for real

To release, follow [RELEASING.md](RELEASING.md), which has every command and what to do when a step fails. In short:

1. Set the new version in three places: `version` in `pyproject.toml`, `version` in `packages/gravity-schema/pyproject.toml`, and the `gravity-schema==` pin in `pyproject.toml`. Run `uv lock`, then merge to `main`.
2. Tag the merge commit and push the tag. A tag containing `rc`, such as `v0.2.0rc1`, publishes to TestPyPI. Any other `v*` tag, such as `v0.2.0`, publishes to PyPI and creates a GitHub Release.

[`release.yml`](.github/workflows/release.yml) runs the full CI first, refuses a tag that does not match all three versions, and publishes with PyPI trusted publishing, so no PyPI token is stored anywhere.

Each package is published from its own GitHub environment:

| Index | Project | Environment |
|---|---|---|
| pypi.org | `gravity-cli` | `pypi` |
| pypi.org | `gravity-schema` | `pypi-schema` |
| test.pypi.org | `gravity-cli` | `testpypi` |
| test.pypi.org | `gravity-schema` | `testpypi-schema` |

To install a release candidate, whose dependencies live on the real PyPI:

```bash
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ gravity-cli==0.2.0rc1
```

After changing `catalog.yaml`, regenerate the tool lists in the platform repository:

```bash
uv run python -m gravity_schema.gen_catalog --root <platform repo>/code-first
```

</details>

## Documentation

| Document | What it covers |
|---|---|
| [Builder guide](src/gravity_cli/BUILDER_GUIDE.md) | How to build a good agent, start to finish, with a complete example. Also `gravity guide` |
| [Agent standard](AGENT_STANDARD.md) | The exact rules an agent must follow |
| [Templates](TEMPLATES.md) | The four templates and when to pick each |
| [`manifest.schema.json`](packages/gravity-schema/schema/manifest.schema.json) | The manifest as a machine-readable JSON Schema |

<br>

---

<p align="center">
  <sub><a href="https://pypi.org/project/gravity-cli/">PyPI</a> · Apache License 2.0 · Copyright 2026 AIGravity · See <a href="LICENSE">LICENSE</a> and <a href="NOTICE">NOTICE</a></sub>
</p>
