gravity-cli 0.4.4__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.
Files changed (105) hide show
  1. gravity_cli-0.4.4/README.md → gravity_cli-0.5.0/PKG-INFO +47 -5
  2. gravity_cli-0.4.4/PKG-INFO → gravity_cli-0.5.0/README.md +30 -22
  3. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/__init__.py +6 -0
  4. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/catalog.yaml +16 -0
  5. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/errors.py +2 -0
  6. gravity_cli-0.5.0/packages/gravity-schema/gravity_schema/source_scan.py +83 -0
  7. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/validation.py +23 -2
  8. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/pyproject.toml +1 -1
  9. gravity_cli-0.5.0/packages/gravity-schema/tests/test_source_scan.py +67 -0
  10. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_validation.py +25 -0
  11. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/pyproject.toml +2 -2
  12. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/BUILDER_GUIDE.md +27 -12
  13. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/cli.py +2 -0
  14. gravity_cli-0.5.0/src/gravity_cli/commands/connect.py +78 -0
  15. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/tools.py +15 -2
  16. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/validate.py +31 -37
  17. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/config.py +33 -8
  18. gravity_cli-0.5.0/src/gravity_cli/manifest.py +22 -0
  19. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/ui.py +3 -0
  20. gravity_cli-0.5.0/tests/test_connect.py +61 -0
  21. gravity_cli-0.5.0/tests/test_session.py +80 -0
  22. gravity_cli-0.5.0/tests/test_validate.py +128 -0
  23. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/uv.lock +2 -2
  24. gravity_cli-0.4.4/src/gravity_cli/manifest.py +0 -9
  25. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/.env.example +0 -0
  26. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/.github/workflows/ci.yml +0 -0
  27. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/.github/workflows/release.yml +0 -0
  28. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/.gitignore +0 -0
  29. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/AGENT_STANDARD.md +0 -0
  30. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/LICENSE +0 -0
  31. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/NOTICE +0 -0
  32. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/RELEASING.md +0 -0
  33. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/TEMPLATES.md +0 -0
  34. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/LICENSE +0 -0
  35. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/NOTICE +0 -0
  36. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/README.md +0 -0
  37. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/__main__.py +0 -0
  38. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/catalog.py +0 -0
  39. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/catalog_imported.yaml +0 -0
  40. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/contract.py +0 -0
  41. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/cron.py +0 -0
  42. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/export.py +0 -0
  43. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/gen_catalog.py +0 -0
  44. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/gravity_schema/manifest.py +0 -0
  45. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/schema/manifest.schema.json +0 -0
  46. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/tests/conftest.py +0 -0
  47. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_catalog_parity.py +0 -0
  48. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_cron.py +0 -0
  49. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_errors.py +0 -0
  50. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_json_schema.py +0 -0
  51. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_kind_outputs.py +0 -0
  52. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_manifest.py +0 -0
  53. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/packages/gravity-schema/tests/test_slots.py +0 -0
  54. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/__init__.py +0 -0
  55. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/__main__.py +0 -0
  56. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/_runner_script.py +0 -0
  57. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/api.py +0 -0
  58. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/__init__.py +0 -0
  59. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/adapt.py +0 -0
  60. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/build.py +0 -0
  61. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/guide.py +0 -0
  62. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/init.py +0 -0
  63. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/login.py +0 -0
  64. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/push.py +0 -0
  65. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/run.py +0 -0
  66. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/runs.py +0 -0
  67. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/commands/schedule.py +0 -0
  68. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/project.py +0 -0
  69. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/chat/.gitignore +0 -0
  70. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/chat/manifest.yaml +0 -0
  71. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/chat/requirements.txt +0 -0
  72. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/chat/src/agent.py +0 -0
  73. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/deepagent/.gitignore +0 -0
  74. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/deepagent/manifest.yaml +0 -0
  75. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/deepagent/requirements.txt +0 -0
  76. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/deepagent/src/agent.py +0 -0
  77. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/minimal/.gitignore +0 -0
  78. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/minimal/manifest.yaml +0 -0
  79. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/minimal/requirements.txt +0 -0
  80. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/minimal/src/agent.py +0 -0
  81. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/.gitignore +0 -0
  82. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/manifest.yaml +0 -0
  83. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/requirements.txt +0 -0
  84. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/src/agent.py +0 -0
  85. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/src/gateway.py +0 -0
  86. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/src/nodes/agent.py +0 -0
  87. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/src/prompts.py +0 -0
  88. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/templates/structured/src/state.py +0 -0
  89. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/src/gravity_cli/timeline.py +0 -0
  90. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/starter/manifest.yaml +0 -0
  91. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/starter/requirements.txt +0 -0
  92. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/starter/src/agent.py +0 -0
  93. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/starter/src/summary.py +0 -0
  94. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/starter/tests/test_summary.py +0 -0
  95. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/tests/conftest.py +0 -0
  96. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/tests/test_adapt.py +0 -0
  97. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/tests/test_api.py +0 -0
  98. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/tests/test_build_push.py +0 -0
  99. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/tests/test_gateway_token.py +0 -0
  100. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/tests/test_manifest_integration.py +0 -0
  101. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/tests/test_run.py +0 -0
  102. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/tests/test_templates.py +0 -0
  103. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/tests/test_timeline.py +0 -0
  104. {gravity_cli-0.4.4 → gravity_cli-0.5.0}/tests/test_ui.py +0 -0
  105. {gravity_cli-0.4.4 → 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, the tool catalog, pushed agents, builds and hosted runs. | `login`, `logout`, `whoami`, `push`, `tools`, `agents`, `runs`, `logs` |
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
- - warnings, never failures, for tool names or network libraries in your code that the manifest does not declare
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 a description and the access each one needs.
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`, `tools`, `agents`, `runs`, `logs`, `whoami` | `gravity login` | nothing |
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,7 @@ 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.
717
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`.
718
760
  - **0.4.3** Documents the `secret` input type and the `supadata.get_transcript` tool.
719
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`).
@@ -1,20 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: gravity-cli
3
- Version: 0.4.4
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.4
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, the tool catalog, pushed agents, builds and hosted runs. | `login`, `logout`, `whoami`, `push`, `tools`, `agents`, `runs`, `logs` |
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
- - warnings, never failures, for tool names or network libraries in your code that the manifest does not declare
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 a description and the access each one needs.
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`, `tools`, `agents`, `runs`, `logs`, `whoami` | `gravity login` | nothing |
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,7 @@ 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.
734
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`.
735
743
  - **0.4.3** Documents the `secret` input type and the `supadata.get_transcript` tool.
736
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`).
@@ -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
+ ]
@@ -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(
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gravity-schema"
3
- version = "0.4.4"
3
+ version = "0.5.0"
4
4
  description = "Gravity agent manifest contract — models, validation, and JSON Schema export"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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.4.4"
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.4.4", # released in lockstep: always the same version as this package
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",
@@ -278,7 +278,7 @@ If any of these is broken, the agent fails when we load or run it.
278
278
  | Environment variables are read inside `graph()` | `token = os.environ[...]` inside it | reading them at module level |
279
279
  | Imports are absolute from `src/` | `from summary import count_by_sender` | `from .summary import ...`, `from src.summary import ...` |
280
280
  | Every tool you call is in the manifest | `gmail.send` listed under `permissions.tools` | calling a tool you didn't list |
281
- | Tools come only from the gateway, models only from `GRAVITY_LLM_URL` | `tools["gmail_read"]`, `ChatOpenAI(base_url=...)` | `requests.post(...)`, your own API keys, `OPENAI_API_KEY` |
281
+ | Accounts are reached only through tools, models only through `GRAVITY_LLM_URL` | `tools["gmail_read"]`, `ChatOpenAI(base_url=...)` | calling Gmail's API with your own credentials, your own API keys, `OPENAI_API_KEY` |
282
282
  | The final state sets `result` | `return {"result": {...}}` | a last node that returns nothing |
283
283
  | No Gravity imports | only your code and your dependencies | there's nothing to import anyway |
284
284
  | Only declare `messages` in the state if the agent is a conversation | see §6.3 | adding it "just in case" (the agent is then run as a chat) |
@@ -610,7 +610,8 @@ return {"result": result, "memory": {"seen": sorted(seen | delivered)[-1000:]}}
610
610
 
611
611
  Your agent can be for any field: sales, marketing, finance, support, engineering, operations,
612
612
  anything. **The table below is today's starting set, not the limit.** If your agent needs a tool
613
- that isn't here, use it anyway and describe it as in §7.1; we connect it for you.
613
+ that isn't here, run `gravity tools <app>` first: the gateway serves about 1,100 more tools for the
614
+ same apps. If it still isn't there, ask for it as in §7.1.
614
615
 
615
616
  | App | Tools |
616
617
  |---|---|
@@ -621,12 +622,21 @@ that isn't here, use it anyway and describe it as in §7.1; we connect it for yo
621
622
  | Linear | `linear.list_teams`, `linear.list_issues`, `linear.get_issue`, `linear.create_issue` |
622
623
  | Slack | `slack.list_channels`, `slack.read_history`, `slack.send`, `slack.find_user` |
623
624
  | GitHub (read) | `github.list_commits`, `github.get_commit`, `github.compare`, `github.list_pull_requests`, `github.list_pr_files`, `github.list_pr_reviews`, `github.search_issues` |
625
+ | Web | `web.search`, `web.fetch` (need no account) |
624
626
  | Test | `example.echo` (needs no account) |
625
627
 
626
628
  - In `manifest.yaml` use the dotted name (`gmail.read`); in code use `_` (`tools["gmail_read"]`).
627
629
  - Your own pure-Python helpers (scoring, parsing, dedupe) need no tool; put them in `src/`.
628
- - Only the gateway is reachable from where your agent runs: direct network calls (`requests`,
629
- `httpx`, sockets) will fail. Anything outside your own code goes through a tool.
630
+ - **Imported tools need an approval to write.** The tools beyond this table are imported from
631
+ the vendor and unreviewed. Any imported tool that changes something must also be listed under
632
+ `approvals.required_for`, or the run is refused with `APPROVAL_REQUIRED`. Reads need nothing
633
+ extra. `gravity validate` checks this.
634
+ - **Outbound network is open.** Your code may read public pages or public APIs directly (`httpx`,
635
+ a search library). Anything that acts on a user's account still goes through a tool, so the
636
+ user's connection and approvals apply. `web.search` and `web.fetch` do the same through the
637
+ gateway with nothing to install.
638
+ - **Connect your own accounts** for local runs with `gravity connect gmail` (or `slack`,
639
+ `sheets`, `calendar`, `linear`...). `gravity connect` on its own lists what is connected.
630
640
 
631
641
  ### 7.1 Using a tool that isn't listed
632
642
 
@@ -635,10 +645,11 @@ Do three things; we handle the rest when your files arrive.
635
645
  1. **Name it** `app.action`, lowercase, with underscores inside a word if needed:
636
646
  `notion.create_page`, `hubspot.find_contact`, `github.list_pull_requests`. Use a plain name
637
647
  for what it does, not the app's own API name.
638
- 2. **Use it like any other tool:** list it under `permissions.tools` in `manifest.yaml` (and under
639
- `approvals.required_for` if it changes something), and call it in code with `_` for `.`:
640
- `tools["notion_create_page"]`. Pass the arguments you describe in step 3 and parse the result
641
- with `_payload`.
648
+ 2. **Write the code that calls it**, with `_` for `.`: `tools.get("notion_create_page")`. Pass the
649
+ arguments you describe in step 3 and parse the result with `_payload`. Leave it out of
650
+ `manifest.yaml` for now: `gravity validate` and the build refuse a tool that isn't in the
651
+ catalog yet. Once we've added it, declare it under `permissions.tools` (and under
652
+ `approvals.required_for` if it changes something).
642
653
  3. **Describe it** in a `tools_needed.yaml` file in the folder's root, one entry per tool:
643
654
 
644
655
  ```yaml
@@ -674,8 +685,8 @@ two actions are two tools.
674
685
  **What happens next:** we connect each tool to the real app. If an argument name or the result
675
686
  shape has to change to fit the real app, we tell you exactly what to change, and it's usually a
676
687
  line or two. A few kinds of tool we may not add, such as bulk deletes or anything that moves
677
- money; we'll say so and suggest an alternative. Because the tool doesn't exist yet on our side,
678
- a run before we've added it fails with `TOOL_UNAVAILABLE`. That's expected.
688
+ money; we'll say so and suggest an alternative. Once it's added, upgrade the CLI so `gravity
689
+ validate` knows the name: `uv tool install --force --refresh gravity-cli`.
679
690
 
680
691
  ## 8. What you can check yourself
681
692
 
@@ -715,7 +726,10 @@ get the result, or what to fix. Messages you may see:
715
726
  |---|---|---|
716
727
  | `TOOL_NOT_DECLARED` | Your code called a tool that isn't in the manifest | Add it to `permissions.tools` (or the adapter's list), or stop calling it |
717
728
  | `NEEDS_RECONNECT` | Our test account for that app isn't connected | Nothing on your side; we'll connect it |
718
- | `TOOL_UNAVAILABLE` | That tool isn't available yet | For a listed tool, check the spelling against §7. For a new one, we're adding it (§7.1) |
729
+ | `TOOL_NOT_FOUND` | A tool in `manifest.yaml` isn't in the catalog | Check the spelling with `gravity tools`. For a new one, ask for it (§7.1) |
730
+ | `APPROVAL_REQUIRED` | An imported write tool has no approval | List it under `approvals.required_for`, with a fallback (§6.1) |
731
+ | `UNDECLARED_TOOL_CALL` | Your code names a write tool the manifest doesn't grant | Declare it (and gate it), or remove the call |
732
+ | `TOOL_UNAVAILABLE` | That tool isn't available yet | Check the spelling against §7, or wait until we've added it (§7.1) |
719
733
  | `APPROVAL_REJECTED` / `APPROVAL_TIMEOUT` | A gated call wasn't approved | Expected; your fallback should have run |
720
734
  | "the final state has no result" | Your last node didn't set `result` | Return `{"result": ...}` |
721
735
  | "could not load the agent" | An import error, a relative import, or building the graph at import | Follow §4 |
@@ -724,7 +738,8 @@ get the result, or what to fix. Messages you may see:
724
738
  ## 11. Checklist before you send
725
739
 
726
740
  - [ ] `manifest.yaml`: `contract: v1`, a clear `description`, clear input labels, `python: "3.12"`
727
- - [ ] Every tool the code calls is declared; nothing makes its own network calls
741
+ - [ ] Every tool the code calls is declared; every account action goes through a tool
742
+ - [ ] Every imported write tool is under `approvals.required_for`
728
743
  - [ ] Any tool not in §7's list is described in `tools_needed.yaml` (§7.1)
729
744
  - [ ] `src/agent.py` defines `async def graph()`; env vars read inside it; imports absolute from `src/`
730
745
  - [ ] Every tool result goes through `_payload`; a failed source is named in the result
@@ -9,6 +9,7 @@ from typer.core import TyperCommand, TyperGroup
9
9
  from . import Problem, config, ui
10
10
  from .commands.adapt import adapt_command
11
11
  from .commands.build import build_command
12
+ from .commands.connect import connect_command
12
13
  from .commands.guide import guide_command
13
14
  from .commands.init import TEMPLATES, init_command
14
15
  from .commands.login import login_command, logout_command, whoami_command
@@ -85,6 +86,7 @@ for name, command, panel, summary, example in [
85
86
  ("login", login_command, ACCOUNT, "Sign in to Gravity in your browser", None),
86
87
  ("logout", logout_command, ACCOUNT, "Sign out", None),
87
88
  ("whoami", whoami_command, ACCOUNT, "Show who you are signed in as", None),
89
+ ("connect", connect_command, ACCOUNT, "Connect an account your agents act on (Gmail, Slack...)", "gravity connect gmail"),
88
90
  ("guide", guide_command, HELP, "Read the builder guide: how to build a good agent", "gravity guide --save"),
89
91
  ]:
90
92
  app.command(name, cls=HelpCommand, help=summary, epilog=example, rich_help_panel=panel)(command)