gravity-cli 0.4.3__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gravity_cli-0.4.3/README.md → gravity_cli-0.5.0/PKG-INFO +49 -6
- gravity_cli-0.4.3/PKG-INFO → gravity_cli-0.5.0/README.md +32 -23
- gravity_cli-0.5.0/RELEASING.md +125 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/__init__.py +6 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/catalog.yaml +16 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/errors.py +2 -0
- gravity_cli-0.5.0/packages/gravity-schema/gravity_schema/source_scan.py +83 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/validation.py +23 -2
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/pyproject.toml +1 -1
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_manifest.py +9 -0
- gravity_cli-0.5.0/packages/gravity-schema/tests/test_source_scan.py +67 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_validation.py +25 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/pyproject.toml +2 -2
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/BUILDER_GUIDE.md +27 -12
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/cli.py +2 -0
- gravity_cli-0.5.0/src/gravity_cli/commands/connect.py +78 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/tools.py +15 -2
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/validate.py +31 -37
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/config.py +33 -8
- gravity_cli-0.5.0/src/gravity_cli/manifest.py +22 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/ui.py +3 -0
- gravity_cli-0.5.0/tests/test_connect.py +61 -0
- gravity_cli-0.5.0/tests/test_session.py +80 -0
- gravity_cli-0.5.0/tests/test_validate.py +128 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/uv.lock +2 -2
- gravity_cli-0.4.3/dist-schema/.gitignore +0 -1
- gravity_cli-0.4.3/dist-schema/gravity_schema-0.4.3-py3-none-any.whl +0 -0
- gravity_cli-0.4.3/dist-schema/gravity_schema-0.4.3.tar.gz +0 -0
- gravity_cli-0.4.3/src/gravity_cli/manifest.py +0 -9
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/.env.example +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/.github/workflows/ci.yml +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/.github/workflows/release.yml +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/.gitignore +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/AGENT_STANDARD.md +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/LICENSE +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/NOTICE +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/TEMPLATES.md +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/LICENSE +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/NOTICE +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/README.md +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/__main__.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/catalog.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/catalog_imported.yaml +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/contract.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/cron.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/export.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/gen_catalog.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/manifest.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/schema/manifest.schema.json +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/tests/conftest.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_catalog_parity.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_cron.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_errors.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_json_schema.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_kind_outputs.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_slots.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/__init__.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/__main__.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/_runner_script.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/api.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/__init__.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/adapt.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/build.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/guide.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/init.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/login.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/push.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/run.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/runs.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/commands/schedule.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/project.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/chat/.gitignore +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/chat/manifest.yaml +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/chat/requirements.txt +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/chat/src/agent.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/deepagent/.gitignore +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/deepagent/manifest.yaml +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/deepagent/requirements.txt +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/deepagent/src/agent.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/minimal/.gitignore +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/minimal/manifest.yaml +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/minimal/requirements.txt +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/minimal/src/agent.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/.gitignore +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/manifest.yaml +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/requirements.txt +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/src/agent.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/src/gateway.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/src/nodes/agent.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/src/prompts.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/src/state.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/src/gravity_cli/timeline.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/starter/manifest.yaml +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/starter/requirements.txt +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/starter/src/agent.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/starter/src/summary.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/starter/tests/test_summary.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/tests/conftest.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/tests/test_adapt.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/tests/test_api.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/tests/test_build_push.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/tests/test_gateway_token.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/tests/test_manifest_integration.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/tests/test_run.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/tests/test_templates.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/tests/test_timeline.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/tests/test_ui.py +0 -0
- {gravity_cli-0.4.3 → gravity_cli-0.5.0}/tests/test_welcome.py +0 -0
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: gravity-cli
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: gravity cli — init, run, and push python agents to the Gravity marketplace
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: NOTICE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: gravity-schema==0.5.0
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: keyring>=25.0.0
|
|
12
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
13
|
+
Requires-Dist: rich>=13.9.0
|
|
14
|
+
Requires-Dist: typer>=0.15.0
|
|
15
|
+
Requires-Dist: uv>=0.8.0
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
1
18
|
<h1 align="center">Gravity CLI</h1>
|
|
2
19
|
|
|
3
20
|
<p align="center">
|
|
@@ -161,8 +178,8 @@ gravity push
|
|
|
161
178
|
|
|
162
179
|
| Service | What it does | Used by |
|
|
163
180
|
|---|---|---|
|
|
164
|
-
| **Gateway** | Serves tools over MCP (`/mcp`) and models over an OpenAI-compatible API (`/v1`). Issues run tokens and refuses any tool a run was not granted. | `run`, `schedule`, `adapt`, and your agent |
|
|
165
|
-
| **Control plane** | Accounts,
|
|
181
|
+
| **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 |
|
|
182
|
+
| **Control plane** | Accounts, pushed agents, builds and hosted runs. | `login`, `logout`, `whoami`, `push`, `agents`, `runs`, `logs` |
|
|
166
183
|
|
|
167
184
|
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.
|
|
168
185
|
|
|
@@ -190,6 +207,7 @@ Run `gravity help` for this list in the terminal, and add `-h` to any command fo
|
|
|
190
207
|
| **Account** | [`gravity login`](#gravity-login) | Sign in to Gravity in your browser |
|
|
191
208
|
| | [`gravity logout`](#gravity-logout) | Sign out |
|
|
192
209
|
| | [`gravity whoami`](#gravity-whoami) | Show who you are signed in as |
|
|
210
|
+
| | [`gravity connect`](#gravity-connect) | Connect an account your agents act on (Gmail, Slack...) |
|
|
193
211
|
| **Help** | [`gravity guide`](#gravity-guide) | Read the builder guide: how to build a good agent |
|
|
194
212
|
| | [`gravity help`](#gravity-help) | Show the list of commands |
|
|
195
213
|
|
|
@@ -238,7 +256,10 @@ What it checks:
|
|
|
238
256
|
- the entrypoint file exists and defines the object the manifest names
|
|
239
257
|
- `requirements.txt` exists, and compiles into `requirements.lock` for the platform image (ARM64 Linux) and the manifest's Python version
|
|
240
258
|
- every adapter a slot declares has a file, and every adapter file is declared
|
|
241
|
-
-
|
|
259
|
+
- 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
|
|
260
|
+
- every imported write tool is under `approvals.required_for`
|
|
261
|
+
- your code never names a write tool the manifest does not grant
|
|
262
|
+
- a warning, never a failure, for read tools your code names but the manifest does not declare
|
|
242
263
|
|
|
243
264
|
```bash
|
|
244
265
|
gravity validate
|
|
@@ -376,7 +397,9 @@ gravity logs <run_id>
|
|
|
376
397
|
|
|
377
398
|
### `gravity tools`
|
|
378
399
|
|
|
379
|
-
Browse the tool catalog: the names you can put under `permissions.tools`, with
|
|
400
|
+
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.
|
|
401
|
+
|
|
402
|
+
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.
|
|
380
403
|
|
|
381
404
|
```
|
|
382
405
|
gravity tools [search]
|
|
@@ -412,6 +435,23 @@ gravity whoami
|
|
|
412
435
|
gravity whoami --token # print the gateway token itself, for CI or a teammate's .env
|
|
413
436
|
```
|
|
414
437
|
|
|
438
|
+
### `gravity connect`
|
|
439
|
+
|
|
440
|
+
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.
|
|
441
|
+
|
|
442
|
+
```
|
|
443
|
+
gravity connect [app]
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
gravity connect # what is connected, and whose accounts runs act on
|
|
448
|
+
gravity connect gmail # opens the sign-in page, then waits until it is done
|
|
449
|
+
gravity connect sheets # use the app name from tool names: sheets, calendar, drive, linear, slack, github...
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
- Uses the gateway token from `gravity login`.
|
|
453
|
+
- `web` tools need no account.
|
|
454
|
+
|
|
415
455
|
### `gravity guide`
|
|
416
456
|
|
|
417
457
|
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.
|
|
@@ -472,7 +512,8 @@ Never commit a `.env` file. The `.gitignore` in every template keeps `.env` and
|
|
|
472
512
|
|---|---|---|---|
|
|
473
513
|
| Create, check and package an agent | `init`, `validate`, `build` | nothing | nothing |
|
|
474
514
|
| Run an agent locally | `run`, `schedule`, `adapt` | `gravity login` (or `GRAVITY_DEV_TOKEN`) | nothing |
|
|
475
|
-
| Publish and inspect agents | `push`, `
|
|
515
|
+
| Publish and inspect agents | `push`, `agents`, `runs`, `logs`, `whoami` | `gravity login` | nothing |
|
|
516
|
+
| Browse tools, connect accounts | `tools`, `connect` | nothing for `tools`; `gravity login` for `connect` | nothing |
|
|
476
517
|
|
|
477
518
|
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).
|
|
478
519
|
|
|
@@ -714,6 +755,8 @@ Real tools act on a connected account. Connect one with `gravity-gateway connect
|
|
|
714
755
|
|
|
715
756
|
## What's new
|
|
716
757
|
|
|
758
|
+
- **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.
|
|
759
|
+
- **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`.
|
|
717
760
|
- **0.4.3** Documents the `secret` input type and the `supadata.get_transcript` tool.
|
|
718
761
|
- **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`).
|
|
719
762
|
|
|
@@ -756,7 +799,7 @@ Every pull request and every push to `main` runs [`ci.yml`](.github/workflows/ci
|
|
|
756
799
|
- both test suites on Ubuntu and Windows, with Python 3.12 and 3.13
|
|
757
800
|
- a build of both packages, installed into a clean environment and used for real
|
|
758
801
|
|
|
759
|
-
To release:
|
|
802
|
+
To release, follow [RELEASING.md](RELEASING.md), which has every command and what to do when a step fails. In short:
|
|
760
803
|
|
|
761
804
|
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`.
|
|
762
805
|
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.
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: gravity-cli
|
|
3
|
-
Version: 0.4.3
|
|
4
|
-
Summary: gravity cli — init, run, and push python agents to the Gravity marketplace
|
|
5
|
-
License-Expression: Apache-2.0
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
License-File: NOTICE
|
|
8
|
-
Requires-Python: >=3.12
|
|
9
|
-
Requires-Dist: gravity-schema==0.4.3
|
|
10
|
-
Requires-Dist: httpx>=0.27.0
|
|
11
|
-
Requires-Dist: keyring>=25.0.0
|
|
12
|
-
Requires-Dist: python-dotenv>=1.0.0
|
|
13
|
-
Requires-Dist: rich>=13.9.0
|
|
14
|
-
Requires-Dist: typer>=0.15.0
|
|
15
|
-
Requires-Dist: uv>=0.8.0
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
|
|
18
1
|
<h1 align="center">Gravity CLI</h1>
|
|
19
2
|
|
|
20
3
|
<p align="center">
|
|
@@ -178,8 +161,8 @@ gravity push
|
|
|
178
161
|
|
|
179
162
|
| Service | What it does | Used by |
|
|
180
163
|
|---|---|---|
|
|
181
|
-
| **Gateway** | Serves tools over MCP (`/mcp`) and models over an OpenAI-compatible API (`/v1`). Issues run tokens and refuses any tool a run was not granted. | `run`, `schedule`, `adapt`, and your agent |
|
|
182
|
-
| **Control plane** | Accounts,
|
|
164
|
+
| **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 |
|
|
165
|
+
| **Control plane** | Accounts, pushed agents, builds and hosted runs. | `login`, `logout`, `whoami`, `push`, `agents`, `runs`, `logs` |
|
|
183
166
|
|
|
184
167
|
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.
|
|
185
168
|
|
|
@@ -207,6 +190,7 @@ Run `gravity help` for this list in the terminal, and add `-h` to any command fo
|
|
|
207
190
|
| **Account** | [`gravity login`](#gravity-login) | Sign in to Gravity in your browser |
|
|
208
191
|
| | [`gravity logout`](#gravity-logout) | Sign out |
|
|
209
192
|
| | [`gravity whoami`](#gravity-whoami) | Show who you are signed in as |
|
|
193
|
+
| | [`gravity connect`](#gravity-connect) | Connect an account your agents act on (Gmail, Slack...) |
|
|
210
194
|
| **Help** | [`gravity guide`](#gravity-guide) | Read the builder guide: how to build a good agent |
|
|
211
195
|
| | [`gravity help`](#gravity-help) | Show the list of commands |
|
|
212
196
|
|
|
@@ -255,7 +239,10 @@ What it checks:
|
|
|
255
239
|
- the entrypoint file exists and defines the object the manifest names
|
|
256
240
|
- `requirements.txt` exists, and compiles into `requirements.lock` for the platform image (ARM64 Linux) and the manifest's Python version
|
|
257
241
|
- every adapter a slot declares has a file, and every adapter file is declared
|
|
258
|
-
-
|
|
242
|
+
- 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
|
|
243
|
+
- every imported write tool is under `approvals.required_for`
|
|
244
|
+
- your code never names a write tool the manifest does not grant
|
|
245
|
+
- a warning, never a failure, for read tools your code names but the manifest does not declare
|
|
259
246
|
|
|
260
247
|
```bash
|
|
261
248
|
gravity validate
|
|
@@ -393,7 +380,9 @@ gravity logs <run_id>
|
|
|
393
380
|
|
|
394
381
|
### `gravity tools`
|
|
395
382
|
|
|
396
|
-
Browse the tool catalog: the names you can put under `permissions.tools`, with
|
|
383
|
+
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.
|
|
384
|
+
|
|
385
|
+
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.
|
|
397
386
|
|
|
398
387
|
```
|
|
399
388
|
gravity tools [search]
|
|
@@ -429,6 +418,23 @@ gravity whoami
|
|
|
429
418
|
gravity whoami --token # print the gateway token itself, for CI or a teammate's .env
|
|
430
419
|
```
|
|
431
420
|
|
|
421
|
+
### `gravity connect`
|
|
422
|
+
|
|
423
|
+
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.
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
gravity connect [app]
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
gravity connect # what is connected, and whose accounts runs act on
|
|
431
|
+
gravity connect gmail # opens the sign-in page, then waits until it is done
|
|
432
|
+
gravity connect sheets # use the app name from tool names: sheets, calendar, drive, linear, slack, github...
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
- Uses the gateway token from `gravity login`.
|
|
436
|
+
- `web` tools need no account.
|
|
437
|
+
|
|
432
438
|
### `gravity guide`
|
|
433
439
|
|
|
434
440
|
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.
|
|
@@ -489,7 +495,8 @@ Never commit a `.env` file. The `.gitignore` in every template keeps `.env` and
|
|
|
489
495
|
|---|---|---|---|
|
|
490
496
|
| Create, check and package an agent | `init`, `validate`, `build` | nothing | nothing |
|
|
491
497
|
| Run an agent locally | `run`, `schedule`, `adapt` | `gravity login` (or `GRAVITY_DEV_TOKEN`) | nothing |
|
|
492
|
-
| Publish and inspect agents | `push`, `
|
|
498
|
+
| Publish and inspect agents | `push`, `agents`, `runs`, `logs`, `whoami` | `gravity login` | nothing |
|
|
499
|
+
| Browse tools, connect accounts | `tools`, `connect` | nothing for `tools`; `gravity login` for `connect` | nothing |
|
|
493
500
|
|
|
494
501
|
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).
|
|
495
502
|
|
|
@@ -731,6 +738,8 @@ Real tools act on a connected account. Connect one with `gravity-gateway connect
|
|
|
731
738
|
|
|
732
739
|
## What's new
|
|
733
740
|
|
|
741
|
+
- **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.
|
|
742
|
+
- **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`.
|
|
734
743
|
- **0.4.3** Documents the `secret` input type and the `supadata.get_transcript` tool.
|
|
735
744
|
- **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`).
|
|
736
745
|
|
|
@@ -773,7 +782,7 @@ Every pull request and every push to `main` runs [`ci.yml`](.github/workflows/ci
|
|
|
773
782
|
- both test suites on Ubuntu and Windows, with Python 3.12 and 3.13
|
|
774
783
|
- a build of both packages, installed into a clean environment and used for real
|
|
775
784
|
|
|
776
|
-
To release:
|
|
785
|
+
To release, follow [RELEASING.md](RELEASING.md), which has every command and what to do when a step fails. In short:
|
|
777
786
|
|
|
778
787
|
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`.
|
|
779
788
|
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.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Releasing gravity-cli and gravity-schema
|
|
2
|
+
|
|
3
|
+
Both packages live in this repository and always ship together at the same version. The only way to publish is a tag on `main`. Nothing is uploaded to PyPI by hand.
|
|
4
|
+
|
|
5
|
+
## 1. Make the change on a branch
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git checkout main && git pull
|
|
9
|
+
git checkout -b <your-branch>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Edit code, tests and docs. If you touched the manifest model in `packages/gravity-schema`, regenerate the committed JSON schema, or the schema tests fail:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cd packages/gravity-schema && uv run python -m gravity_schema && cd ../..
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 2. Check locally before you push
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv run ruff check .
|
|
22
|
+
uv run pytest -q
|
|
23
|
+
cd packages/gravity-schema && uv run pytest -q && cd ../..
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
All green, or it does not go up.
|
|
27
|
+
|
|
28
|
+
## 3. Bump the version in both packages
|
|
29
|
+
|
|
30
|
+
Three places must carry the same number:
|
|
31
|
+
|
|
32
|
+
| File | Line |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `pyproject.toml` | `version = "X.Y.Z"` |
|
|
35
|
+
| `pyproject.toml` | `"gravity-schema==X.Y.Z"` under `dependencies` |
|
|
36
|
+
| `packages/gravity-schema/pyproject.toml` | `version = "X.Y.Z"` |
|
|
37
|
+
|
|
38
|
+
Then refresh the lockfile:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv lock
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Patch for fixes and docs, minor for new commands or manifest fields. Check PyPI first so a number is never reused:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
curl -s https://pypi.org/pypi/gravity-cli/json | python -c "import sys,json;print(json.load(sys.stdin)['info']['version'])"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 4. Open a pull request against `main`
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git push -u origin <your-branch>
|
|
54
|
+
gh pr create --base main
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
CI runs lint, package and the tests on Ubuntu and Windows with Python 3.12 and 3.13. Merge when green.
|
|
58
|
+
|
|
59
|
+
## 5. Tag the merge commit on `main`
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git checkout main && git pull
|
|
63
|
+
git tag vX.Y.Z
|
|
64
|
+
git push origin vX.Y.Z
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The release workflow (`.github/workflows/release.yml`) then:
|
|
68
|
+
|
|
69
|
+
1. checks the tag against both versions and the pin, and stops if any disagree;
|
|
70
|
+
2. reruns the full CI;
|
|
71
|
+
3. builds both wheels and sdists;
|
|
72
|
+
4. publishes to PyPI through trusted publishing, schema first, then CLI;
|
|
73
|
+
5. creates the GitHub release with the four files attached.
|
|
74
|
+
|
|
75
|
+
Watch it:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
gh run list --workflow release.yml --limit 1
|
|
79
|
+
gh run watch <run-id>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 6. Verify from a clean install
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
uv tool install --force --refresh gravity-cli==X.Y.Z
|
|
86
|
+
gravity whoami
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Dry run on TestPyPI
|
|
90
|
+
|
|
91
|
+
Any tag containing `rc` publishes to TestPyPI instead of PyPI:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git tag vX.Y.Zrc1 && git push origin vX.Y.Zrc1
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Use this when you change the workflow itself or the packaging.
|
|
98
|
+
|
|
99
|
+
## If something goes wrong
|
|
100
|
+
|
|
101
|
+
**The guard fails with "tag says X but version is Y".** You tagged before bumping. Delete the tag, fix the versions, retag.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
git tag -d vX.Y.Z && git push origin :refs/tags/vX.Y.Z
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**A job fails on "other side closed" or a download error.** A GitHub hiccup. Rerun the failed jobs, nothing else is needed.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
gh run rerun <run-id> --failed
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Publish fails with "file already exists".** That version is already on PyPI and cannot be overwritten. Bump and release again.
|
|
114
|
+
|
|
115
|
+
**Wrong code shipped.** Do not delete the PyPI release. Ship a fixed version on top, and yank the bad one on PyPI only if it is harmful.
|
|
116
|
+
|
|
117
|
+
## Rules
|
|
118
|
+
|
|
119
|
+
- No copies of the source in other repositories. Pin the PyPI version in your requirements instead.
|
|
120
|
+
- No PyPI API tokens. The trusted publisher configured for this repository is the only credential. If you hold a token for either package, say so and it gets revoked.
|
|
121
|
+
- Every change is a pull request, however small. Review turnaround is same day.
|
|
122
|
+
|
|
123
|
+
## Record
|
|
124
|
+
|
|
125
|
+
Versions 0.4.1 to 0.4.3 were published to PyPI on 2026-10-08 from a copy of this source kept in the core repository. Their changes were ported back in PR #11. This repository has no tag or GitHub release for them, and the next release from here is 0.4.4.
|
|
@@ -36,7 +36,9 @@ from .manifest import (
|
|
|
36
36
|
StateSpec,
|
|
37
37
|
TriggersSpec,
|
|
38
38
|
)
|
|
39
|
+
from .source_scan import is_agent_source, undeclared_write_tools
|
|
39
40
|
from .validation import (
|
|
41
|
+
check_imported_write_approvals,
|
|
40
42
|
check_tool_references,
|
|
41
43
|
load_manifest_file,
|
|
42
44
|
load_manifest_text,
|
|
@@ -73,6 +75,10 @@ __all__ = [ # noqa: RUF022 - grouped by concern, not alphabetical
|
|
|
73
75
|
"load_manifest_text",
|
|
74
76
|
"load_manifest_file",
|
|
75
77
|
"check_tool_references",
|
|
78
|
+
"check_imported_write_approvals",
|
|
79
|
+
# source scan
|
|
80
|
+
"is_agent_source",
|
|
81
|
+
"undeclared_write_tools",
|
|
76
82
|
# catalog
|
|
77
83
|
"ToolCatalog",
|
|
78
84
|
"StaticToolCatalog",
|
|
@@ -27,6 +27,22 @@
|
|
|
27
27
|
guarantee: certified
|
|
28
28
|
side_effect: read
|
|
29
29
|
|
|
30
|
+
- name: web.search
|
|
31
|
+
slug: COMPOSIO_SEARCH_WEB
|
|
32
|
+
description: search the web; returns results with titles, links and snippets. No account needed
|
|
33
|
+
scopes: []
|
|
34
|
+
breadth: generic
|
|
35
|
+
guarantee: certified
|
|
36
|
+
side_effect: read
|
|
37
|
+
|
|
38
|
+
- name: web.fetch
|
|
39
|
+
slug: COMPOSIO_SEARCH_FETCH_URL_CONTENT
|
|
40
|
+
description: read the text content of a web page by its URL. No account needed
|
|
41
|
+
scopes: []
|
|
42
|
+
breadth: generic
|
|
43
|
+
guarantee: certified
|
|
44
|
+
side_effect: read
|
|
45
|
+
|
|
30
46
|
- name: supadata.get_transcript
|
|
31
47
|
slug: null
|
|
32
48
|
description: fetch the transcript of a YouTube video with the caller's own Supadata API key (api_key argument)
|
|
@@ -27,6 +27,8 @@ class ErrorCode(str, Enum):
|
|
|
27
27
|
INVALID_TOOL_NAME = "INVALID_TOOL_NAME"
|
|
28
28
|
DUPLICATE_TOOL = "DUPLICATE_TOOL"
|
|
29
29
|
TOOL_NOT_FOUND = "TOOL_NOT_FOUND"
|
|
30
|
+
UNDECLARED_TOOL_CALL = "UNDECLARED_TOOL_CALL"
|
|
31
|
+
APPROVAL_REQUIRED = "APPROVAL_REQUIRED"
|
|
30
32
|
INVALID_MODEL_TIER = "INVALID_MODEL_TIER"
|
|
31
33
|
INVALID_RESOURCES = "INVALID_RESOURCES"
|
|
32
34
|
INVALID_TRIGGER = "INVALID_TRIGGER"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Agent code that calls a write tool its manifest does not grant.
|
|
2
|
+
|
|
3
|
+
The gateway refuses such a call at run time (TOOL_NOT_DECLARED). This finds it
|
|
4
|
+
before the build, from the source text alone: nothing is imported or run.
|
|
5
|
+
`gravity validate` and the platform's build step share it, so a builder who
|
|
6
|
+
skips the CLI is still stopped.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
from collections.abc import Iterable, Iterator
|
|
13
|
+
from pathlib import PurePath
|
|
14
|
+
|
|
15
|
+
from .catalog import CATALOG, ToolEntry
|
|
16
|
+
from .errors import ErrorCode, ManifestIssue, Severity
|
|
17
|
+
from .manifest import Manifest
|
|
18
|
+
|
|
19
|
+
# Not shipped as agent behaviour: tests may name any tool to assert it is refused.
|
|
20
|
+
SKIPPED_DIRS = frozenset({".venv", "venv", "tests", "__pycache__", ".gravity"})
|
|
21
|
+
|
|
22
|
+
# Manifests say 'gmail.send'; code says 'gmail_send' (some models reject dots in
|
|
23
|
+
# function names). Either spelling is the same tool.
|
|
24
|
+
_BY_SPELLING: dict[str, ToolEntry] = {
|
|
25
|
+
spelling: e for e in CATALOG for spelling in (e.name, e.name.replace(".", "_"))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_agent_source(path: PurePath) -> bool:
|
|
30
|
+
return path.suffix == ".py" and not SKIPPED_DIRS & set(path.parts)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _docstrings(tree: ast.Module) -> set[int]:
|
|
34
|
+
owners = [tree, *(n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)))]
|
|
35
|
+
return {
|
|
36
|
+
id(owner.body[0].value)
|
|
37
|
+
for owner in owners
|
|
38
|
+
if owner.body
|
|
39
|
+
and isinstance(owner.body[0], ast.Expr)
|
|
40
|
+
and isinstance(owner.body[0].value, ast.Constant)
|
|
41
|
+
and isinstance(owner.body[0].value.value, str)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _string_literals(tree: ast.Module) -> Iterator[str]:
|
|
46
|
+
"""Every string constant except docstrings, which describe rather than call."""
|
|
47
|
+
skip = _docstrings(tree)
|
|
48
|
+
for node in ast.walk(tree):
|
|
49
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str) and id(node) not in skip:
|
|
50
|
+
yield node.value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def undeclared_write_tools(sources: Iterable[tuple[str, str]], manifest: Manifest) -> list[ManifestIssue]:
|
|
54
|
+
"""One issue per catalog write tool named in `sources` ((path, text) pairs)
|
|
55
|
+
that no part of the manifest grants. Reads stay the drift scan's warning."""
|
|
56
|
+
declared = manifest.declared_tools
|
|
57
|
+
first_seen: dict[str, str] = {}
|
|
58
|
+
for path, text in sources:
|
|
59
|
+
try:
|
|
60
|
+
tree = ast.parse(text, filename=path)
|
|
61
|
+
except SyntaxError:
|
|
62
|
+
continue # the entrypoint check reports a broken entrypoint; this scan has nothing to read
|
|
63
|
+
for literal in _string_literals(tree):
|
|
64
|
+
entry = _BY_SPELLING.get(literal)
|
|
65
|
+
if entry and entry.side_effect == "write" and entry.name not in declared:
|
|
66
|
+
first_seen.setdefault(entry.name, path)
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
ManifestIssue(
|
|
70
|
+
code=ErrorCode.UNDECLARED_TOOL_CALL,
|
|
71
|
+
location=path,
|
|
72
|
+
message=f"{path} calls '{tool}', a write tool the manifest does not grant.",
|
|
73
|
+
remediation=(
|
|
74
|
+
f"Declare '{tool}' under permissions.tools (and approvals.required_for, so a person "
|
|
75
|
+
"says yes before it acts), or remove the call. If it only delivers the finished "
|
|
76
|
+
"result, list it under an output's destinations instead: the platform sends it "
|
|
77
|
+
"from the user's own connected account after the run."
|
|
78
|
+
),
|
|
79
|
+
value=tool,
|
|
80
|
+
severity=Severity.HIGH,
|
|
81
|
+
)
|
|
82
|
+
for tool, path in sorted(first_seen.items())
|
|
83
|
+
]
|
{gravity_cli-0.4.3 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/validation.py
RENAMED
|
@@ -9,7 +9,7 @@ import yaml
|
|
|
9
9
|
from pydantic import ValidationError
|
|
10
10
|
|
|
11
11
|
from . import contract as C
|
|
12
|
-
from .catalog import ToolCatalog
|
|
12
|
+
from .catalog import CATALOG, ToolCatalog
|
|
13
13
|
from .errors import (
|
|
14
14
|
ErrorCode,
|
|
15
15
|
ManifestIssue,
|
|
@@ -56,7 +56,28 @@ def check_tool_references(manifest: Manifest, catalog: ToolCatalog) -> list[Mani
|
|
|
56
56
|
)
|
|
57
57
|
)
|
|
58
58
|
|
|
59
|
-
return issues
|
|
59
|
+
return issues + check_imported_write_approvals(manifest)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Imported tools (catalog_imported.yaml) are unreviewed. The gateway refuses a run that is
|
|
63
|
+
# granted an imported write tool without a human approval in front of it (APPROVAL_REQUIRED).
|
|
64
|
+
_IMPORTED_WRITES = frozenset(e.name for e in CATALOG if e.guarantee == "experimental" and e.side_effect == "write")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_imported_write_approvals(manifest: Manifest) -> list[ManifestIssue]:
|
|
68
|
+
"""One issue per imported write tool the agent may call that no approval covers."""
|
|
69
|
+
gated = set(manifest.approvals.required_for)
|
|
70
|
+
return [
|
|
71
|
+
ManifestIssue(
|
|
72
|
+
code=ErrorCode.APPROVAL_REQUIRED,
|
|
73
|
+
location="approvals.required_for",
|
|
74
|
+
message=f"'{name}' is an imported write tool, so a person must approve each call.",
|
|
75
|
+
remediation=f"Add '{name}' under approvals.required_for, with a fallback for when it isn't approved.",
|
|
76
|
+
value=name,
|
|
77
|
+
severity=Severity.HIGH,
|
|
78
|
+
)
|
|
79
|
+
for name in sorted(manifest.declared_tools & _IMPORTED_WRITES - gated)
|
|
80
|
+
]
|
|
60
81
|
|
|
61
82
|
|
|
62
83
|
def validate_manifest(
|
|
@@ -624,3 +624,12 @@ def test_state_size_outside_bounds_rejected(manifest_data, bad):
|
|
|
624
624
|
def test_runtime_python_defaults_to_313(manifest_data):
|
|
625
625
|
del manifest_data["runtime"]["python"]
|
|
626
626
|
assert validate_manifest(manifest_data).runtime.python == "3.12"
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def test_a_secret_input_cannot_have_a_default(manifest_data):
|
|
630
|
+
manifest_data["inputs"] = [{"name": "api_key", "type": "secret", "label": "Key", "default": "sk-x"}]
|
|
631
|
+
with pytest.raises(ManifestValidationError) as e:
|
|
632
|
+
validate_manifest(manifest_data)
|
|
633
|
+
assert "cannot have a default" in str(e.value)
|
|
634
|
+
del manifest_data["inputs"][0]["default"]
|
|
635
|
+
assert validate_manifest(manifest_data).inputs[0].type.value == "secret"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import PurePosixPath
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from gravity_schema import Manifest, is_agent_source, undeclared_write_tools
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def manifest(tools: list[str], adapters: dict[str, list[str]] | None = None) -> Manifest:
|
|
10
|
+
data = {
|
|
11
|
+
"name": "scan-agent",
|
|
12
|
+
"version": "1.0.0",
|
|
13
|
+
"runtime": {"python": "3.12", "entrypoint": "src/agent.py:graph"},
|
|
14
|
+
"permissions": {"tools": tools},
|
|
15
|
+
}
|
|
16
|
+
if adapters:
|
|
17
|
+
data["slots"] = {"deliver": {"label": "Deliver to", "default": next(iter(adapters)), "adapters": adapters}}
|
|
18
|
+
return Manifest.model_validate(data)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def found(source: str, m: Manifest) -> list[str]:
|
|
22
|
+
return [i.value for i in undeclared_write_tools([("src/agent.py", source)], m)]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.mark.parametrize("spelling", ["gmail.send", "gmail_send"])
|
|
26
|
+
def test_an_undeclared_send_is_found_in_either_spelling(spelling):
|
|
27
|
+
assert found(f'TOOL = "{spelling}"\n', manifest(["gmail.read"])) == ["gmail.send"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_a_declared_send_is_not_reported():
|
|
31
|
+
assert found('TOOL = "gmail_send"\n', manifest(["gmail.send"])) == []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_a_tool_an_adapter_declares_is_granted():
|
|
35
|
+
m = manifest([], {"slack": ["slack.send"]})
|
|
36
|
+
assert found('TOOL = "slack_send"\n', m) == []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_reads_are_left_to_the_drift_warning():
|
|
40
|
+
assert found('TOOL = "gmail_read"\n', manifest([])) == []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_docstrings_describe_rather_than_call():
|
|
44
|
+
source = '"""Never "gmail.send"."""\n\n\ndef f():\n """Uses gmail_send? No."""\n return 1\n'
|
|
45
|
+
assert found(source, manifest([])) == []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_each_tool_is_reported_once_with_where_it_was_first_seen():
|
|
49
|
+
sources = [("src/a.py", 'X = "slack.send"\n'), ("src/b.py", 'Y = "slack_send"\nZ = "gmail.send"\n')]
|
|
50
|
+
issues = undeclared_write_tools(sources, manifest([]))
|
|
51
|
+
assert [(i.value, i.location, i.code.value) for i in issues] == [
|
|
52
|
+
("gmail.send", "src/b.py", "UNDECLARED_TOOL_CALL"),
|
|
53
|
+
("slack.send", "src/a.py", "UNDECLARED_TOOL_CALL"),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_a_file_that_does_not_parse_is_skipped():
|
|
58
|
+
assert found("def broken(:\n 'gmail.send'\n", manifest([])) == []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize(
|
|
62
|
+
("path", "shipped"),
|
|
63
|
+
[("src/agent.py", True), ("agent.py", True), ("tests/test_x.py", False),
|
|
64
|
+
(".venv/lib/x.py", False), ("src/__pycache__/x.py", False), ("README.md", False)],
|
|
65
|
+
)
|
|
66
|
+
def test_only_shipped_python_is_scanned(path, shipped):
|
|
67
|
+
assert is_agent_source(PurePosixPath(path)) is shipped
|
|
@@ -170,3 +170,28 @@ def test_missing_file_reports_clearly(tmp_path):
|
|
|
170
170
|
|
|
171
171
|
assert "gravity init" in (ei.value.issues[0].remediation or "")
|
|
172
172
|
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_an_imported_write_tool_needs_an_approval(manifest_data, catalog):
|
|
176
|
+
"""The gateway refuses a run granted an unreviewed write tool without a human approval."""
|
|
177
|
+
manifest_data["permissions"]["tools"] = ["gmail.read", "github.add_assignees_to_an_issue"]
|
|
178
|
+
with pytest.raises(ManifestValidationError) as e:
|
|
179
|
+
validate_manifest(manifest_data, catalog=catalog)
|
|
180
|
+
issues = [i for i in e.value.issues if i.code == ErrorCode.APPROVAL_REQUIRED]
|
|
181
|
+
assert [i.value for i in issues] == ["github.add_assignees_to_an_issue"]
|
|
182
|
+
|
|
183
|
+
manifest_data["approvals"] = {"required_for": ["github.add_assignees_to_an_issue"]}
|
|
184
|
+
validate_manifest(manifest_data, catalog=catalog)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_curated_writes_and_imported_reads_need_no_approval(manifest_data, catalog):
|
|
188
|
+
manifest_data["permissions"]["tools"] = ["gmail.send", "github.get_a_repository", "web.search", "web.fetch"]
|
|
189
|
+
manifest_data.pop("approvals", None)
|
|
190
|
+
validate_manifest(manifest_data, catalog=catalog)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_a_tool_in_no_catalog_is_refused(manifest_data, catalog):
|
|
194
|
+
manifest_data["permissions"]["tools"] = ["composio_search.web"]
|
|
195
|
+
with pytest.raises(ManifestValidationError) as e:
|
|
196
|
+
validate_manifest(manifest_data, catalog=catalog)
|
|
197
|
+
assert {i.code for i in e.value.issues} == {ErrorCode.TOOL_NOT_FOUND}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "gravity-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "gravity cli — init, run, and push python agents to the Gravity marketplace"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
license-files = ["LICENSE", "NOTICE"]
|
|
8
8
|
requires-python = ">=3.12"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"gravity-schema==0.
|
|
10
|
+
"gravity-schema==0.5.0", # released in lockstep: always the same version as this package
|
|
11
11
|
"typer>=0.15.0",
|
|
12
12
|
"httpx>=0.27.0",
|
|
13
13
|
"rich>=13.9.0",
|