Metadata-Version: 2.5
Name: gravity-schema
Version: 0.12.3
Summary: Gravity agent manifest contract — models, validation, and JSON Schema export
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Requires-Python: >=3.11
Requires-Dist: pydantic>=2.10
Requires-Dist: pyyaml>=6.0
Description-Content-Type: text/markdown

# gravity-schema

The Gravity agent manifest contract: its models, validation, the capability vocabulary and JSON Schema export.
It is the one definition the [gravity CLI](https://pypi.org/project/gravity-cli/), the agent runner and Gravity's
platform share, so a manifest that passes here passes everywhere. If you build agents, use `gravity-cli`: it
depends on this package, in the same version.

```bash
pip install gravity-schema
```

## Two contracts

The manifest's `contract` picks one, and Gravity runs both side by side.

- **v1** (`Manifest`): the agent names the **tools** it calls, such as `gmail.send`. A manifest without
  `contract` is v1.
- **v2** (`ManifestV2`): the agent names **capabilities**, such as `message.list` or `issue.create`. What it reads
  goes under `sources` and what it changes under `writes`, and the person picks the app (Gmail or Outlook, Linear
  or Jira) when they set it up. That setup is an `Instance`.

## Load and validate a manifest

```python
from gravity_schema import load_manifest_text

manifest = load_manifest_text("""
contract: v2
name: inbox-digest
version: 1.0.0
description: "A morning digest of my inbox"
runtime: {framework: langgraph, python: "3.12", entrypoint: src/agent.py:graph}
dependencies: {lockfile: requirements.lock}
sources:
  inbox: {capability: message.list, label: "Which inbox?", window: 1d}
writes:
  digest: {capability: message.send, label: "Where should the digest go?"}
approvals: {required_for: [message.send]}
""")
manifest.contract               # 'v2'
manifest.capabilities           # frozenset({'message.list', 'message.send'})
```

`load_manifest_file(path)` reads a file instead. Both return whichever contract the manifest declares, and raise
`ManifestValidationError` with its `ManifestIssue`s (each a code, location, message and remediation) when it
doesn't fit. Unknown fields are rejected, so a typo is reported rather than ignored.

## The capability vocabulary (v2)

`VOCABULARY` holds every capability and which apps serve it.

```python
from gravity_schema import VOCABULARY

VOCABULARY.apps_for("message.list")     # ('gmail', 'instagram', 'microsoft_teams', 'outlook', ...)
```

## A consumer's setup and a run's grant (v2)

```python
from datetime import UTC, datetime
from gravity_schema import Instance, check_instance, grant, run_token_body

setup = Instance.model_validate({
    "agent": "inbox-digest", "version": "1.0.0",
    "sources": {"inbox": {"app": "gmail"}},
    "writes": {"digest": {"app": "gmail"}},
})
check_instance(manifest, setup)          # []: the setup fits (otherwise, what doesn't)
grant(manifest, setup).as_dict()         # exactly what one run may call, and what waits for approval
run_token_body(manifest, setup, now=datetime.now(UTC))   # {'bindings', 'grant', 'approvals'} for a run token
```

## Explain an agent to its users (v2)

These write plain text from the manifest and the setup, with no model involved.

```python
from gravity_schema import capabilities_summary, permissions

capabilities_summary(manifest)
# {'can': ['Read your messages', 'Send a message'], 'cant': [...], 'also': [...]}
permissions(manifest, setup)
# per app: what it reads, what it changes, what asks first, and what it never does
```

`live` describes a running setup.

## Draw a setup screen (v2)

`setup_plan` is the screen a consumer sets an agent up on, shortest first: the agent's parts grouped by the app they
use, with the apps each group could switch to, then only the places that must be picked. The rest (time ranges,
filters, optional parts, more outputs) is under `advanced`. Pass `connections` (app → connected) to mark each group.

```python
from gravity_schema import Instance, VOCABULARY, apply_group_change, setup_plan

setup = Instance.model_validate({"agent": "inbox-digest", "version": "1.0.0",
                                 "sources": {"inbox": {"app": "gmail"}}, "writes": {"digest": {"app": "slack"}}})
plan = setup_plan(manifest, setup, connections={"gmail": True})
plan["groups"]    # [{app: 'gmail', connected: True, parts: [sources.inbox], ...}, {app: 'slack', connected: False, ...}]
plan["places"]    # [{app: 'slack', resource: 'channel', says: 'Choose the channel in Slack', ...}]
plan["advanced"]  # {places, windows, filters, inputs, optional, outputs}

# "Switch Slack to Teams": every part of the group moves; the Slack channel means nothing in Teams, so it is cleared
moved, issues = apply_group_change(manifest, setup, "slack", "microsoft_teams")   # issues: a Teams chat to pick

VOCABULARY.place_maker("googlesheets", "spreadsheet")   # "Create a new ...": {within, within_required, columns}
VOCABULARY.place_maker("slack", "channel")              # None: not made from a name
VOCABULARY.mapped_apps_for("message.send")              # ('gmail', 'instagram', 'microsoft_teams', 'outlook', 'slack')
```

## JSON Schema

```python
from gravity_schema import manifest_json_schema

schema = manifest_json_schema()          # one schema covering v1 and v2
```

Or from the command line, `python -m gravity_schema --out manifest.schema.json`. Add `--check` to exit 1 when
that file is out of date.

## Versions

gravity-schema and gravity-cli are released together, always in the same version. The changes are in the CLI's
changelog (`gravity changelog`).
