Skip to main content

Gravity CLI

Build, test and publish AI agents on Gravity.

Write ordinary LangGraph, declare what your agent may touch in one manifest,
and run it against real tools with those permissions enforced.

PyPI Python License

pipx install gravity-cli
gravity init my-agent

There is no SDK. Your agent reads three environment variables (GRAVITY_GATEWAY_URL, GRAVITY_LLM_URL, GRAVITY_RUN_TOKEN), and gravity is the only Gravity-specific tool you use.

Here is gravity run on an agent that tried a tool it never declared. The gateway refused the 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

New to building agents? Start with the builder guide. The exact rules are in the agent standard, and templates helps you pick a starting point.

Contents

How it works

gravity init      scaffold a working agent from a template
gravity validate  check the manifest, entrypoint, lockfile and adapters, locally
gravity run       run it through the gateway, with only the declared tools allowed
gravity build     bundle it into a deterministic build/agent.zip
gravity push      upload exactly that bundle to the control plane

Three services are involved. The CLI talks to two of them; your agent talks only to the gateway.

Service What it does Who talks to it
Gateway Serves tools over MCP (/mcp) and models over an OpenAI-compatible API (/v1). Mints run tokens and refuses any tool a run was not granted. gravity run mints a token; your agent makes every tool and model call through it
Control plane Accounts, the tool catalog, pushed agents, builds and hosted runs. login, push, tools, agents, runs, logs
Your agent Your LangGraph code, started by gravity run in its own virtual environment. Sees only a run token scoped to its declared tools

On gravity run, the CLI trades your dev token for a run token scoped to permissions.tools and starts the agent with only that token. A call to an undeclared tool is refused by the gateway, exactly as it will be when the agent runs hosted. Calls listed under approvals.required_for pause until you approve them in the terminal.

Installation

Requires Python 3.12 or newer, and uv for your agents' own environments.

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. Upgrade with pipx upgrade gravity-cli or uv tool upgrade gravity-cli.

Quick start

  1. Set up ~/.gravity/.env as described in Environment variables. At minimum, gravity run needs GRAVITY_DEV_TOKEN.
  2. Make sure a gateway is reachable: either a local one or the hosted dev gateway.
  3. Then:
gravity init my-agent              # creates ./my-agent from the minimal template
cd my-agent
uv venv && uv pip install -r requirements.txt

gravity validate                   # offline checks, compiles requirements.lock
gravity run --input query="hello"  # live timeline of steps, model and tool calls
gravity build                      # writes build/agent.zip and build/build.json
gravity login                      # once, before your first push
gravity push                       # uploads the build; refuses if it is stale

Environment variables

Where to set them

Put them in ~/.gravity/.env (on Windows, %USERPROFILE%\.gravity\.env). Every gravity command loads this file, from any directory, so you set a value once and never export it again. .env.example in this repository is a commented template you can copy there:

mkdir -p ~/.gravity
cp .env.example ~/.gravity/.env      # then fill in the values

When the same variable is set 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 URL, written by gravity --api-url URL <command>)
  4. the built-in default

Never commit a .env file. This repository's .gitignore, and the one every template ships with, keep .env and .gravity/ out of git.

Which ones you need

You want to... Commands Required Usually also set
Scaffold, check and package an agent init, validate, build nothing —
Run an agent locally run, schedule, adapt GRAVITY_DEV_TOKEN GRAVITY_GATEWAY_URL, unless the gateway is local on port 8000
Publish and inspect agents push, tools, agents, runs, logs, whoami gravity login, or GRAVITY_API_TOKEN GRAVITY_API_URL, unless the control plane is at http://localhost:3010

Reference

Variable What it is Required Default
GRAVITY_DEV_TOKEN Your gateway dev token. gravity run trades it for a run token scoped to the manifest's tools. A local gateway prints one at startup, or uses its LOCAL_DEV_TOKEN. For run, schedule, adapt none
GRAVITY_GATEWAY_URL The gateway's MCP endpoint, ending in /mcp. The CLI derives the gateway's base URL from it by removing /mcp. When the gateway is not local http://127.0.0.1:8000/mcp
GRAVITY_LLM_URL The OpenAI-compatible model endpoint. Override it per run with --llm-url. No the gateway's base URL + /v1
GRAVITY_API_URL The control plane's base URL. When the control plane is not local http://localhost:3010
GRAVITY_API_TOKEN The control plane's dev token. Commands then run as the control plane's dev user without gravity login, and it wins over a stored session. For development only. Instead of gravity login none
GRAVITY_CONFIG_DIR Moves ~/.gravity elsewhere: the .env file, config.json and the fallback session file. Useful for tests and CI. No ~/.gravity
GRAVITY_UNATTENDED When 1, gated tool calls are rejected without prompting, as if nobody were at the terminal. gravity schedule sets it for you. No unset

Set for your agent, never by you

gravity run starts your agent with exactly these three, and the hosted runner does the same:

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, scoped to the declared tools. Use it as the bearer token for tools and as the API key for models.

Your own GRAVITY_DEV_TOKEN and GRAVITY_API_TOKEN are removed from the agent's environment, so agent code can never act as you. Read the three variables inside graph(), not at import time; see the agent standard.

Example setups

Everything on your machine (local gateway and control plane):

# ~/.gravity/.env
GRAVITY_DEV_TOKEN=<printed by gravity-gateway serve, or its LOCAL_DEV_TOKEN>
GRAVITY_API_URL=http://localhost:3010
GRAVITY_API_TOKEN=<the control plane's PLATFORM_DEV_TOKEN>

Hosted dev gateway:

# ~/.gravity/.env
GRAVITY_GATEWAY_URL=https://<hosted gateway host>/mcp
GRAVITY_DEV_TOKEN=<the dev token you were issued>
GRAVITY_API_URL=https://<control plane host>

GRAVITY_LLM_URL is left out in both: it follows the gateway.

Running a local gateway

The gateway is part of the Gravity platform repository, not this one. It is open to the Gravity team; outside builders use the hosted dev gateway instead. Start it with gravity-gateway serve; it listens on port 8000 and prints a dev token.

It reads its own .env file in the gateway's directory. None of its variables is needed for a first run: with nothing set it serves only the built-in example.echo tool, keeps tokens in memory, and prints a new dev token at every start.

Variable What it enables When you need it
COMPOSIO_API_KEY Real tools (Gmail, Slack, Linear and others) through Composio. Unset: only example.echo. To call any real tool
OPENROUTER_API_KEY Model calls on /v1, forwarded to OpenRouter. 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. Put the same value in ~/.gravity/.env as GRAVITY_DEV_TOKEN. Recommended
GATEWAY_PORT The listening port. Default 8000. If you change it, set GRAVITY_GATEWAY_URL=http://127.0.0.1:<port>/mcp. Only if 8000 is taken
DATABASE_URL, PLATFORM_DB_SCHEMA Keeps run tokens in the platform's Postgres database instead of memory. Platform development only
PLATFORM_SERVICE_KEY, AWS_REGION Lets the platform's hosted runs mint tokens and report back. Platform development only
SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY The older token store, used when DATABASE_URL is unset. Legacy setups only

Real tools act on a connected account. On a local gateway every run uses one shared test identity; connect an account to it with gravity-gateway connect --user-id builder:local --toolkit <app>. See the gateway's own README for details.

Authentication

There are two separate credentials, for two separate services:

Credential Used for How you get it
Control-plane session push, tools, agents, runs, logs, whoami gravity login, or GRAVITY_API_TOKEN for development
Gateway dev token (GRAVITY_DEV_TOKEN) run, schedule, adapt Printed by a local gateway, or issued for the hosted one
  • gravity login opens the control plane's sign-in page and waits for you to approve a device code. The session is stored in the OS keychain, or in ~/.gravity/session.json (readable only by you) on machines without one. gravity logout removes it.
  • GRAVITY_API_TOKEN set to the control plane's dev token skips login entirely. gravity whoami then prints the dev user's id followed by (dev token).

Commands

Command What it does
gravity init <name> [--template T] [--here] Scaffold a working agent. Templates: minimal, structured, deepagent, chat. --here writes only a manifest into the current directory, for an existing project.
gravity validate [path] Check the manifest, the entrypoint, the declared adapters and the lockfile, and warn about tool names or network imports the manifest does not declare. Compiles requirements.lock for the platform image (ARM64 Linux).
gravity run [path] --input k=v Run the agent locally through the gateway. --input k=@file.txt reads a value from a file. --reply "..." continues the last conversation of a chat agent. --llm-url URL overrides GRAVITY_LLM_URL.
gravity schedule [path] [--cron EXPR] [--now] Run the agent on its triggers.schedule until Ctrl+C. Nobody is at the terminal, so gated calls are not approved.
gravity build [path] Validate, then write build/agent.zip and build/build.json. The same source always produces the same bytes. Nothing leaves your machine.
gravity push [path] Upload what build made. Refuses if there is no build or the source changed since. Published versions are frozen: bump version to publish again.
gravity adapt <slot> <name> --tools a.b,c.d Have a coding agent write a new adapter for a slot. The file is kept only if the slot's conformance tests pass; otherwise nothing changes.
gravity tools [search] List the tool catalog: the names you can put in permissions.tools.
gravity agents Your published agents and their review status.
gravity runs <agent> / gravity logs <run-id> Hosted run history and logs.
gravity login / logout / whoami Sign in to and out of the control plane, and show who you are.

Every command accepts --help. The global option --api-url URL, given before the command, saves a new control-plane URL to ~/.gravity/config.json.

An agent project

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

The manifest

manifest.yaml declares everything about the agent that Gravity enforces: what the consumer fills in, which tools it may call, which calls need approval, and how it is triggered. Only name, runtime.entrypoint and, for a useful agent, permissions.tools are needed; everything else has a default.

The smallest useful manifest:

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]

Every section, annotated. This one is valid as written:

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]

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, e.g. 1.2.0. A published version can never change; bump it to publish again.
description no empty Up to 500 characters.
kind no interactive transform (input in, result out), interactive (calls tools mid-run, may converse), 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.13 3.10 to 3.13 are accepted, but the platform image runs 3.12: set "3.12" before you push.
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 (lowercase identifier, becomes the key in state["inputs"]), type, label (shown in the form), and optionally required, default, options, accept.
inputs[].type yes text, textarea, number, boolean, static-dropdown (needs options), email, url, date, file (no default; accept lists extensions such as [pdf, csv]).
permissions.tools no none Up to 50 catalog names, e.g. gmail.read. Run gravity tools for the list. The run token allows exactly these.
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, e.g. "0 9 * * 1") and consumer_can_change (default true). Try it with gravity schedule.
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. Adapter code lives in src/adapters/<slot>/<adapter>.py. The pick arrives in state["inputs"] under the slot's name, so slot names cannot repeat an input's name.
outputs.<name> no none Up to 8 extra formats the platform renders from the result: renderer (markdown, docx, pptx, rows), label, and destinations (download, or a catalog tool such as gmail.send that the platform calls itself).

Unknown fields are rejected rather than ignored, so a typo is reported instead of silently dropped. gravity validate lists every problem at once, with a suggested fix for each.

Your code

Inputs arrive in state["inputs"], and the final state["result"] is what the consumer gets. Add a messages key to your state to make a chat agent. The exact rules are in the agent standard, and the machine-readable schema is manifest.schema.json.

graph is an async factory, not a graph built at import time: the runner calls it fresh for every run, after that run's environment variables are set.

Files the CLI writes in a project:

Path Written by Contents
requirements.lock validate exact pins for the platform image; commit it
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/ and .gravity/.

Troubleshooting

Message Cause and fix
missing: GRAVITY_DEV_TOKEN run, schedule and adapt need a gateway dev token. Add GRAVITY_DEV_TOKEN to ~/.gravity/.env.
can't reach the gateway at … No gateway at that address. Start a local one with gravity-gateway serve, or set GRAVITY_GATEWAY_URL to the hosted gateway's /mcp URL.
the gateway refused this run — … The gateway rejected the token or a declared tool. A token from a restarted local gateway is no longer valid: set LOCAL_DEV_TOKEN there so it stays fixed.
not logged in — run gravity login first, or set GRAVITY_API_TOKEN. A control-plane command without credentials. Run gravity login, or set GRAVITY_API_TOKEN for development.
missing required input(s): … Pass each required input with --input name=value, or give it a default in the manifest.
no build found / build is stale Run gravity build again after any change to src/, the manifest or the requirements.
`uv pip compile` failed for aarch64-manylinux2014 A dependency has no wheel 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.

Limitations

  • No offline mode. gravity run always talks to a real gateway. There is no mock-tool mode, so every local run needs a gateway, and real tools need a connected account.
  • Static drift check. gravity validate finds undeclared tools by scanning source for imports and tool-shaped strings. Expect 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 hosted runs exist.

Development

git clone https://github.com/AIGravity/gravity-cli.git
cd gravity-cli
uv sync                                   # installs both packages and dev tools
uv run gravity --help                     # or activate .venv to use gravity directly
uv run pytest                             # CLI tests
cd packages/gravity-schema && uv run pytest && cd ../..   # manifest contract tests
uv run ruff check .

After changing the manifest model in packages/gravity-schema, regenerate the committed JSON Schema; a test fails until you do:

uv run python -m gravity_schema            # --check only reports whether it is stale

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

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

Continuous integration

Every pull request and every push to main runs ci.yml:

  • ruff check, and a check that uv.lock matches pyproject.toml
  • both test suites on Ubuntu and Windows, Python 3.12 and 3.13 (these include a check that the committed JSON Schema is current)
  • a build of both packages, installed into a clean environment and used for real (gravity init and gravity validate), which catches files missing from the package before PyPI does

Releasing

gravity-cli and gravity-schema are released together, always at the same version.

  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's dependencies. Run uv lock, then merge to main.
  2. Tag the merge commit and push the tag:
    • git tag v0.1.0rc1 && git push origin v0.1.0rc1 publishes a release candidate to TestPyPI.
    • git tag v0.1.0 && git push origin v0.1.0 publishes to PyPI.
  3. Watch the Release run in the repository's Actions tab. A PyPI release also creates a GitHub Release with generated notes.

release.yml runs the full CI first, refuses a tag that doesn't match all three versions, and publishes with PyPI trusted publishing, so no PyPI token is stored anywhere. Each package is published in its own job and environment, because a trusted publisher can create only one new project per login.

One-time setup, done once by a maintainer:

Where Project Workflow Environment
pypi.org gravity-cli release.yml pypi
pypi.org gravity-schema release.yml pypi-schema
test.pypi.org gravity-cli release.yml testpypi
test.pypi.org gravity-schema release.yml testpypi-schema

All four use the repository AIGravity/gravity-cli. On GitHub, under Settings → Environments, create the four environments named in the last column, each limited to tags matching v*.

To install a release candidate, which has its dependencies on the real PyPI:

pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ gravity-cli==0.1.0rc2

Repository layout

src/gravity_cli/            the CLI
  commands/                 one module per command
  templates/                what `gravity init` copies
  _runner_script.py         runs an agent inside its own venv for `gravity run`
packages/gravity-schema/    the manifest contract: models, validation, tool catalog, JSON Schema
  gravity_schema/catalog.yaml           the curated tool catalog
  gravity_schema/catalog_imported.yaml  generated by the gateway's sync-tools; experimental tools
starter/                    the builder guide's example agent (inbox digest)
tests/                      CLI tests

gravity-schema is its own package because the CLI, the gateway, the hosted runner and the control plane's intake all validate the same manifest against the same rules.

License

Apache License 2.0. See LICENSE and NOTICE.

Metadata

Release files for gravity-cli 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gravity-cli 0.1.0
File Size Uploaded
gravity_cli-0.1.0.tar.gz 210.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gravity-cli 0.1.0
File Interpreter ABI Platform
gravity_cli-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 269.6 kB

Release files / gravity_cli-0.1.0.tar.gz

Download URL gravity_cli-0.1.0.tar.gz
Size 210.4 kB
Tags Source
SHA-256 checksum
How to use checksums
0f22874d93dd628e09ba38227fec02bd4acabbbcd3684982999dee1835415e15
BLAKE2b-256 checksum
How to use checksums
223db508fc9dd78d27b7a4d237fdd40560fe8ce3ce619751caaf9f44f72b8a66
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / gravity_cli-0.1.0-py3-none-any.whl

Download URL gravity_cli-0.1.0-py3-none-any.whl
Size 59.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d5e3cefde505ea78e9ef5f29e4986819aa2153c434e20c4fe778c04ba996e29d
BLAKE2b-256 checksum
How to use checksums
42ab91069e0fb483141481562b3c56917c14f7b35c42907019586fbb806bc6d3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page