gravity-cli 0.3.1__tar.gz → 0.4.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 (98) hide show
  1. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/.env.example +3 -3
  2. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/PKG-INFO +17 -15
  3. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/README.md +15 -13
  4. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/__init__.py +2 -0
  5. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/contract.py +3 -0
  6. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/manifest.py +1 -1
  7. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/pyproject.toml +1 -1
  8. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/schema/manifest.schema.json +1 -1
  9. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_manifest.py +1 -1
  10. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/pyproject.toml +2 -2
  11. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/api.py +12 -2
  12. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/adapt.py +2 -1
  13. gravity_cli-0.4.0/src/gravity_cli/commands/login.py +89 -0
  14. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/run.py +4 -5
  15. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/validate.py +6 -1
  16. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/config.py +2 -1
  17. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/chat/manifest.yaml +1 -1
  18. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/deepagent/manifest.yaml +1 -1
  19. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/minimal/manifest.yaml +1 -1
  20. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/manifest.yaml +1 -1
  21. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_build_push.py +11 -0
  22. gravity_cli-0.4.0/tests/test_gateway_token.py +72 -0
  23. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_templates.py +1 -1
  24. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/uv.lock +6 -6
  25. gravity_cli-0.3.1/src/gravity_cli/commands/login.py +0 -49
  26. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/.github/workflows/ci.yml +0 -0
  27. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/.github/workflows/release.yml +0 -0
  28. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/.gitignore +0 -0
  29. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/AGENT_STANDARD.md +0 -0
  30. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/LICENSE +0 -0
  31. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/NOTICE +0 -0
  32. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/TEMPLATES.md +0 -0
  33. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/LICENSE +0 -0
  34. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/NOTICE +0 -0
  35. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/README.md +0 -0
  36. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/__main__.py +0 -0
  37. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/catalog.py +0 -0
  38. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/catalog.yaml +0 -0
  39. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/catalog_imported.yaml +0 -0
  40. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/cron.py +0 -0
  41. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/errors.py +0 -0
  42. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/export.py +0 -0
  43. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/gen_catalog.py +0 -0
  44. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/validation.py +0 -0
  45. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/conftest.py +0 -0
  46. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_catalog_parity.py +0 -0
  47. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_cron.py +0 -0
  48. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_errors.py +0 -0
  49. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_json_schema.py +0 -0
  50. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_kind_outputs.py +0 -0
  51. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_slots.py +0 -0
  52. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_validation.py +0 -0
  53. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/BUILDER_GUIDE.md +0 -0
  54. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/__init__.py +0 -0
  55. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/__main__.py +0 -0
  56. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/_runner_script.py +0 -0
  57. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/cli.py +0 -0
  58. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/__init__.py +0 -0
  59. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/build.py +0 -0
  60. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/guide.py +0 -0
  61. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/init.py +0 -0
  62. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/push.py +0 -0
  63. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/runs.py +0 -0
  64. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/schedule.py +0 -0
  65. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/tools.py +0 -0
  66. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/manifest.py +0 -0
  67. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/project.py +0 -0
  68. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/chat/.gitignore +0 -0
  69. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/chat/requirements.txt +0 -0
  70. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/chat/src/agent.py +0 -0
  71. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/deepagent/.gitignore +0 -0
  72. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/deepagent/requirements.txt +0 -0
  73. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/deepagent/src/agent.py +0 -0
  74. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/minimal/.gitignore +0 -0
  75. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/minimal/requirements.txt +0 -0
  76. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/minimal/src/agent.py +0 -0
  77. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/.gitignore +0 -0
  78. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/requirements.txt +0 -0
  79. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/src/agent.py +0 -0
  80. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/src/gateway.py +0 -0
  81. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/src/nodes/agent.py +0 -0
  82. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/src/prompts.py +0 -0
  83. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/src/state.py +0 -0
  84. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/timeline.py +0 -0
  85. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/ui.py +0 -0
  86. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/starter/manifest.yaml +0 -0
  87. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/starter/requirements.txt +0 -0
  88. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/starter/src/agent.py +0 -0
  89. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/starter/src/summary.py +0 -0
  90. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/starter/tests/test_summary.py +0 -0
  91. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/conftest.py +0 -0
  92. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_adapt.py +0 -0
  93. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_api.py +0 -0
  94. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_manifest_integration.py +0 -0
  95. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_run.py +0 -0
  96. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_timeline.py +0 -0
  97. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_ui.py +0 -0
  98. {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_welcome.py +0 -0
@@ -3,9 +3,9 @@
3
3
  # What each variable does: README.md, "Environment variables".
4
4
 
5
5
  # --- gravity run / schedule / adapt -------------------------------------
6
- # Your gateway dev token. Required to run an agent. Issued by the Gravity team;
7
- # a local gateway prints one at startup, or uses its LOCAL_DEV_TOKEN.
8
- GRAVITY_DEV_TOKEN=
6
+ # A gateway token. Leave unset: gravity login stores one for you.
7
+ # Set it only for a local gateway (it prints one at startup, or uses its LOCAL_DEV_TOKEN).
8
+ # GRAVITY_DEV_TOKEN=
9
9
 
10
10
  # The gateway's MCP endpoint. Leave unset: the CLI asks Gravity where the gateway is.
11
11
  # Set it only for a local gateway.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gravity-cli
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: gravity cli — init, run, and push python agents to the Gravity marketplace
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
7
7
  License-File: NOTICE
8
8
  Requires-Python: >=3.12
9
- Requires-Dist: gravity-schema==0.3.1
9
+ Requires-Dist: gravity-schema==0.4.0
10
10
  Requires-Dist: httpx>=0.27.0
11
11
  Requires-Dist: keyring>=25.0.0
12
12
  Requires-Dist: python-dotenv>=1.0.0
@@ -69,7 +69,7 @@ pipx install gravity-cli
69
69
  gravity init my-agent
70
70
  ```
71
71
 
72
- > **Status.** `login`, `push` and the tool catalog work out of the box against Gravity's hosted services. `gravity run` also needs a gateway dev token, issued by the Gravity team until per-builder tokens ship.
72
+ > **Status.** `login`, `push`, `run` and the tool catalog work out of the box against Gravity's hosted services. `gravity login` issues you the gateway token that `run` needs; nothing goes in `.env`.
73
73
 
74
74
  ## Contents
75
75
 
@@ -120,7 +120,7 @@ The same rules apply on your laptop and when the agent runs hosted, because both
120
120
  - `graph` is an `async def` factory. Read environment variables inside it, never at import time.
121
121
  - A tool that is not in `permissions.tools` is refused, whatever your code does.
122
122
  - A tool listed in `approvals.required_for` waits for a yes in the terminal before it runs.
123
- - The platform runs Python 3.12. Set `runtime.python: "3.12"` in the manifest before you push.
123
+ - The platform runs Python 3.12. `gravity init` writes that, and `gravity validate` fails on anything else.
124
124
 
125
125
  ## Installation
126
126
 
@@ -284,7 +284,7 @@ gravity run --input transcript=@call.txt --input tone=formal
284
284
  gravity run --reply "make it shorter"
285
285
  ```
286
286
 
287
- - Requires `GRAVITY_DEV_TOKEN` and a reachable gateway.
287
+ - Requires a gateway token: the one `gravity login` stores, or `GRAVITY_DEV_TOKEN`.
288
288
  - A call to a tool in `approvals.required_for` pauses, shows what it would do, and waits for `y` or `n`. With no terminal to ask in, it is rejected.
289
289
  - Exits with code 0 when the run finishes and 1 when it fails.
290
290
 
@@ -422,10 +422,11 @@ gravity logout
422
422
 
423
423
  ### `gravity whoami`
424
424
 
425
- Show the account you are signed in as.
425
+ Show the account you are signed in as, and whether you hold a gateway token and until when.
426
426
 
427
427
  ```
428
428
  gravity whoami
429
+ gravity whoami --token # print the gateway token itself, for CI or a teammate's .env
429
430
  ```
430
431
 
431
432
  ### `gravity guide`
@@ -464,11 +465,11 @@ gravity help
464
465
 
465
466
  ### Where settings live
466
467
 
467
- Put them in `~/.gravity/.env` (on Windows, `%USERPROFILE%\.gravity\.env`). Every `gravity` command loads this file, from any folder, so you set a value once.
468
+ Put them in `~/.gravity/.env` (on Windows, `%USERPROFILE%\.gravity\.env`). Every `gravity` command loads this file, from any folder, so you set a value once. With Gravity's hosted services, `gravity login` is all you need, and the file can stay empty.
468
469
 
469
470
  ```bash
470
471
  # ~/.gravity/.env
471
- GRAVITY_DEV_TOKEN=<your gateway dev token>
472
+ GRAVITY_DEV_TOKEN=<a gateway token, only if gravity login did not issue you one>
472
473
  ```
473
474
 
474
475
  That is the only setting most builders ever need. The CLI knows where Gravity's control plane is, and asks it where the gateway is.
@@ -487,7 +488,7 @@ Never commit a `.env` file. The `.gitignore` in every template keeps `.env` and
487
488
  | You want to | Commands | Required | Usually also set |
488
489
  |---|---|---|---|
489
490
  | Create, check and package an agent | `init`, `validate`, `build` | nothing | nothing |
490
- | Run an agent locally | `run`, `schedule`, `adapt` | `GRAVITY_DEV_TOKEN` | nothing |
491
+ | Run an agent locally | `run`, `schedule`, `adapt` | `gravity login` (or `GRAVITY_DEV_TOKEN`) | nothing |
491
492
  | Publish and inspect agents | `push`, `tools`, `agents`, `runs`, `logs`, `whoami` | `gravity login` | nothing |
492
493
 
493
494
  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).
@@ -496,7 +497,7 @@ Set `GRAVITY_API_URL` and `GRAVITY_GATEWAY_URL` only to work against services yo
496
497
 
497
498
  | Variable | What it is | Default |
498
499
  |---|---|---|
499
- | `GRAVITY_DEV_TOKEN` | Your gateway dev token. `gravity run` trades it for a run token scoped to the manifest's tools. Required for `run`, `schedule` and `adapt`. | none |
500
+ | `GRAVITY_DEV_TOKEN` | A gateway token. `gravity run` trades it for a run token scoped to the manifest's tools. Overrides the token `gravity login` stored; needed only when login did not issue one, such as against a local gateway. | the token from `gravity login` |
500
501
  | `GRAVITY_GATEWAY_URL` | The gateway's MCP endpoint, ending in `/mcp` | the gateway the control plane names in `GET /api/cli/config` |
501
502
  | `GRAVITY_LLM_URL` | The OpenAI-compatible model endpoint | the gateway's address + `/v1` |
502
503
  | `GRAVITY_API_URL` | The control plane's address | `https://agents.dev.gravity.fast` |
@@ -521,9 +522,9 @@ Two separate credentials, for two separate services:
521
522
  | Credential | Used by | How you get it |
522
523
  |---|---|---|
523
524
  | Control-plane session | `push`, `tools`, `agents`, `runs`, `logs`, `whoami` | `gravity login` |
524
- | Gateway dev token | `run`, `schedule`, `adapt` | Issued to you, and stored as `GRAVITY_DEV_TOKEN` in `~/.gravity/.env` |
525
+ | Gateway token | `run`, `schedule`, `adapt` | `gravity login` asks Gravity for one and stores it with the session. `gravity whoami` shows its expiry; `gravity whoami --token` prints it. `GRAVITY_DEV_TOKEN`, if set, is used instead. |
525
526
 
526
- `gravity login` stores the session in the operating system's keychain. On machines without one, it falls back to `~/.gravity/session.json`, readable only by you. `gravity logout` removes it.
527
+ `gravity login` stores the session and gateway token in the operating system's keychain. On machines without one, it falls back to `~/.gravity/session.json`, readable only by you. `gravity logout` removes it.
527
528
 
528
529
  For development, setting `GRAVITY_API_TOKEN` to the control plane's dev token skips login. `gravity whoami` then prints the dev user's id followed by `(dev token)`.
529
530
 
@@ -637,7 +638,7 @@ outputs: # extra formats the platform renders and deliv
637
638
  | `kind` | no | `interactive` | `transform` (input in, result out), `interactive` (calls tools mid-run, may converse) or `monitor` (runs on a schedule or webhook and remembers the last run; needs a trigger). |
638
639
  | `runtime.entrypoint` | **yes** | | `path/to/file.py:object`, inside the project. The object is your `graph` factory. |
639
640
  | `runtime.framework` | no | `langgraph` | Only `langgraph`. |
640
- | `runtime.python` | no | `3.13` | `3.10` to `3.13` are accepted, but the platform image runs **3.12**. Set `"3.12"` before you push. |
641
+ | `runtime.python` | no | `3.12` | What the platform image runs. `gravity validate` fails on any other value, because the hosted build would. |
641
642
  | `runtime.timeout_seconds` | no | `300` | 10 to 28800. |
642
643
  | `dependencies.lockfile` | no | `requirements.lock` | Compiled by `gravity validate`; ships with the agent. |
643
644
  | `inputs[]` | no | none | Up to 50. Each has `name` (a lowercase identifier, the key in `state["inputs"]`), `type`, `label`, and optionally `required`, `default`, `options`, `accept`. |
@@ -717,9 +718,10 @@ Real tools act on a connected account. Connect one with `gravity-gateway connect
717
718
  | Message | Cause and fix |
718
719
  |---|---|
719
720
  | `No matching distribution found for gravity-cli` | pip is running on a Python older than 3.12. Use `uv tool install gravity-cli`, or see [Installation](#installation). |
720
- | `No gateway dev token` | `run`, `schedule` and `adapt` need a gateway dev token. Add `GRAVITY_DEV_TOKEN` to `~/.gravity/.env`. |
721
+ | `No gateway token` | `run`, `schedule` and `adapt` need a gateway token. Run `gravity login`, or add `GRAVITY_DEV_TOKEN` to `~/.gravity/.env`. |
722
+ | `Your gateway token has expired` | The token `gravity login` stored has lapsed. Run `gravity login` again. |
721
723
  | `Can't reach the gateway at …` | No gateway at that address. Set `GRAVITY_GATEWAY_URL` to your gateway's `/mcp` address. |
722
- | `The gateway refused this run` | The gateway rejected the token or a declared tool. Check that `GRAVITY_DEV_TOKEN` is current. |
724
+ | `The gateway refused this run` | The gateway rejected the token or a declared tool. Run `gravity whoami` to check the token, or `gravity login` for a fresh one. |
723
725
  | `You are not signed in` | A control-plane command without credentials. Run `gravity login`. |
724
726
  | `Can't reach Gravity at …` | The control plane did not answer. Check `GRAVITY_API_URL` in `~/.gravity/.env`. |
725
727
  | `Missing required input: …` | Pass each required input with `--input name=value`, or give it a `default` in the manifest. |
@@ -52,7 +52,7 @@ pipx install gravity-cli
52
52
  gravity init my-agent
53
53
  ```
54
54
 
55
- > **Status.** `login`, `push` and the tool catalog work out of the box against Gravity's hosted services. `gravity run` also needs a gateway dev token, issued by the Gravity team until per-builder tokens ship.
55
+ > **Status.** `login`, `push`, `run` and the tool catalog work out of the box against Gravity's hosted services. `gravity login` issues you the gateway token that `run` needs; nothing goes in `.env`.
56
56
 
57
57
  ## Contents
58
58
 
@@ -103,7 +103,7 @@ The same rules apply on your laptop and when the agent runs hosted, because both
103
103
  - `graph` is an `async def` factory. Read environment variables inside it, never at import time.
104
104
  - A tool that is not in `permissions.tools` is refused, whatever your code does.
105
105
  - A tool listed in `approvals.required_for` waits for a yes in the terminal before it runs.
106
- - The platform runs Python 3.12. Set `runtime.python: "3.12"` in the manifest before you push.
106
+ - The platform runs Python 3.12. `gravity init` writes that, and `gravity validate` fails on anything else.
107
107
 
108
108
  ## Installation
109
109
 
@@ -267,7 +267,7 @@ gravity run --input transcript=@call.txt --input tone=formal
267
267
  gravity run --reply "make it shorter"
268
268
  ```
269
269
 
270
- - Requires `GRAVITY_DEV_TOKEN` and a reachable gateway.
270
+ - Requires a gateway token: the one `gravity login` stores, or `GRAVITY_DEV_TOKEN`.
271
271
  - A call to a tool in `approvals.required_for` pauses, shows what it would do, and waits for `y` or `n`. With no terminal to ask in, it is rejected.
272
272
  - Exits with code 0 when the run finishes and 1 when it fails.
273
273
 
@@ -405,10 +405,11 @@ gravity logout
405
405
 
406
406
  ### `gravity whoami`
407
407
 
408
- Show the account you are signed in as.
408
+ Show the account you are signed in as, and whether you hold a gateway token and until when.
409
409
 
410
410
  ```
411
411
  gravity whoami
412
+ gravity whoami --token # print the gateway token itself, for CI or a teammate's .env
412
413
  ```
413
414
 
414
415
  ### `gravity guide`
@@ -447,11 +448,11 @@ gravity help
447
448
 
448
449
  ### Where settings live
449
450
 
450
- Put them in `~/.gravity/.env` (on Windows, `%USERPROFILE%\.gravity\.env`). Every `gravity` command loads this file, from any folder, so you set a value once.
451
+ Put them in `~/.gravity/.env` (on Windows, `%USERPROFILE%\.gravity\.env`). Every `gravity` command loads this file, from any folder, so you set a value once. With Gravity's hosted services, `gravity login` is all you need, and the file can stay empty.
451
452
 
452
453
  ```bash
453
454
  # ~/.gravity/.env
454
- GRAVITY_DEV_TOKEN=<your gateway dev token>
455
+ GRAVITY_DEV_TOKEN=<a gateway token, only if gravity login did not issue you one>
455
456
  ```
456
457
 
457
458
  That is the only setting most builders ever need. The CLI knows where Gravity's control plane is, and asks it where the gateway is.
@@ -470,7 +471,7 @@ Never commit a `.env` file. The `.gitignore` in every template keeps `.env` and
470
471
  | You want to | Commands | Required | Usually also set |
471
472
  |---|---|---|---|
472
473
  | Create, check and package an agent | `init`, `validate`, `build` | nothing | nothing |
473
- | Run an agent locally | `run`, `schedule`, `adapt` | `GRAVITY_DEV_TOKEN` | nothing |
474
+ | Run an agent locally | `run`, `schedule`, `adapt` | `gravity login` (or `GRAVITY_DEV_TOKEN`) | nothing |
474
475
  | Publish and inspect agents | `push`, `tools`, `agents`, `runs`, `logs`, `whoami` | `gravity login` | nothing |
475
476
 
476
477
  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).
@@ -479,7 +480,7 @@ Set `GRAVITY_API_URL` and `GRAVITY_GATEWAY_URL` only to work against services yo
479
480
 
480
481
  | Variable | What it is | Default |
481
482
  |---|---|---|
482
- | `GRAVITY_DEV_TOKEN` | Your gateway dev token. `gravity run` trades it for a run token scoped to the manifest's tools. Required for `run`, `schedule` and `adapt`. | none |
483
+ | `GRAVITY_DEV_TOKEN` | A gateway token. `gravity run` trades it for a run token scoped to the manifest's tools. Overrides the token `gravity login` stored; needed only when login did not issue one, such as against a local gateway. | the token from `gravity login` |
483
484
  | `GRAVITY_GATEWAY_URL` | The gateway's MCP endpoint, ending in `/mcp` | the gateway the control plane names in `GET /api/cli/config` |
484
485
  | `GRAVITY_LLM_URL` | The OpenAI-compatible model endpoint | the gateway's address + `/v1` |
485
486
  | `GRAVITY_API_URL` | The control plane's address | `https://agents.dev.gravity.fast` |
@@ -504,9 +505,9 @@ Two separate credentials, for two separate services:
504
505
  | Credential | Used by | How you get it |
505
506
  |---|---|---|
506
507
  | Control-plane session | `push`, `tools`, `agents`, `runs`, `logs`, `whoami` | `gravity login` |
507
- | Gateway dev token | `run`, `schedule`, `adapt` | Issued to you, and stored as `GRAVITY_DEV_TOKEN` in `~/.gravity/.env` |
508
+ | Gateway token | `run`, `schedule`, `adapt` | `gravity login` asks Gravity for one and stores it with the session. `gravity whoami` shows its expiry; `gravity whoami --token` prints it. `GRAVITY_DEV_TOKEN`, if set, is used instead. |
508
509
 
509
- `gravity login` stores the session in the operating system's keychain. On machines without one, it falls back to `~/.gravity/session.json`, readable only by you. `gravity logout` removes it.
510
+ `gravity login` stores the session and gateway token in the operating system's keychain. On machines without one, it falls back to `~/.gravity/session.json`, readable only by you. `gravity logout` removes it.
510
511
 
511
512
  For development, setting `GRAVITY_API_TOKEN` to the control plane's dev token skips login. `gravity whoami` then prints the dev user's id followed by `(dev token)`.
512
513
 
@@ -620,7 +621,7 @@ outputs: # extra formats the platform renders and deliv
620
621
  | `kind` | no | `interactive` | `transform` (input in, result out), `interactive` (calls tools mid-run, may converse) or `monitor` (runs on a schedule or webhook and remembers the last run; needs a trigger). |
621
622
  | `runtime.entrypoint` | **yes** | | `path/to/file.py:object`, inside the project. The object is your `graph` factory. |
622
623
  | `runtime.framework` | no | `langgraph` | Only `langgraph`. |
623
- | `runtime.python` | no | `3.13` | `3.10` to `3.13` are accepted, but the platform image runs **3.12**. Set `"3.12"` before you push. |
624
+ | `runtime.python` | no | `3.12` | What the platform image runs. `gravity validate` fails on any other value, because the hosted build would. |
624
625
  | `runtime.timeout_seconds` | no | `300` | 10 to 28800. |
625
626
  | `dependencies.lockfile` | no | `requirements.lock` | Compiled by `gravity validate`; ships with the agent. |
626
627
  | `inputs[]` | no | none | Up to 50. Each has `name` (a lowercase identifier, the key in `state["inputs"]`), `type`, `label`, and optionally `required`, `default`, `options`, `accept`. |
@@ -700,9 +701,10 @@ Real tools act on a connected account. Connect one with `gravity-gateway connect
700
701
  | Message | Cause and fix |
701
702
  |---|---|
702
703
  | `No matching distribution found for gravity-cli` | pip is running on a Python older than 3.12. Use `uv tool install gravity-cli`, or see [Installation](#installation). |
703
- | `No gateway dev token` | `run`, `schedule` and `adapt` need a gateway dev token. Add `GRAVITY_DEV_TOKEN` to `~/.gravity/.env`. |
704
+ | `No gateway token` | `run`, `schedule` and `adapt` need a gateway token. Run `gravity login`, or add `GRAVITY_DEV_TOKEN` to `~/.gravity/.env`. |
705
+ | `Your gateway token has expired` | The token `gravity login` stored has lapsed. Run `gravity login` again. |
704
706
  | `Can't reach the gateway at …` | No gateway at that address. Set `GRAVITY_GATEWAY_URL` to your gateway's `/mcp` address. |
705
- | `The gateway refused this run` | The gateway rejected the token or a declared tool. Check that `GRAVITY_DEV_TOKEN` is current. |
707
+ | `The gateway refused this run` | The gateway rejected the token or a declared tool. Run `gravity whoami` to check the token, or `gravity login` for a fresh one. |
706
708
  | `You are not signed in` | A control-plane command without credentials. Run `gravity login`. |
707
709
  | `Can't reach Gravity at …` | The control plane did not answer. Check `GRAVITY_API_URL` in `~/.gravity/.env`. |
708
710
  | `Missing required input: …` | Pass each required input with `--input name=value`, or give it a `default` in the manifest. |
@@ -8,6 +8,7 @@ from .contract import (
8
8
  ENV_LLM_URL,
9
9
  ENV_RUN_TOKEN,
10
10
  LOCKFILE_PLATFORM,
11
+ PLATFORM_PYTHON,
11
12
  SUPPORTED_CONTRACT_VERSIONS,
12
13
  SUPPORTED_PYTHON_VERSIONS,
13
14
  )
@@ -49,6 +50,7 @@ __all__ = [ # noqa: RUF022 - grouped by concern, not alphabetical
49
50
  "SUPPORTED_CONTRACT_VERSIONS",
50
51
  "SUPPORTED_PYTHON_VERSIONS",
51
52
  "LOCKFILE_PLATFORM",
53
+ "PLATFORM_PYTHON",
52
54
  "ENV_GATEWAY_URL",
53
55
  "ENV_LLM_URL",
54
56
  "ENV_RUN_TOKEN",
@@ -53,6 +53,9 @@ SUPPORTED_PYTHON_VERSIONS: Final[frozenset[str]] = frozenset(
53
53
 
54
54
  # The platform image is ARM64 Linux; locks are resolved for it, not the laptop.
55
55
  LOCKFILE_PLATFORM: Final[str] = "aarch64-manylinux2014"
56
+ # The Python the platform image runs (infra/base-image). A manifest naming another
57
+ # version is accepted by the schema but refused by the hosted build, so validate checks it.
58
+ PLATFORM_PYTHON: Final[str] = "3.12"
56
59
 
57
60
  MIN_TIMEOUT_SECONDS: Final[int] = 10
58
61
  # An AgentCore session's 8-hour maximum lifetime.
@@ -79,7 +79,7 @@ class RuntimeSpec(BaseModel):
79
79
  model_config = _STRICT
80
80
 
81
81
  framework: str = "langgraph"
82
- python: str = "3.13"
82
+ python: str = C.PLATFORM_PYTHON
83
83
  entrypoint: Annotated[str, Field(max_length=C.MAX_ENTRYPOINT_LENGTH)]
84
84
  timeout_seconds: Annotated[int, Field(ge=C.MIN_TIMEOUT_SECONDS, le=C.MAX_TIMEOUT_SECONDS)] = 300
85
85
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gravity-schema"
3
- version = "0.3.1"
3
+ version = "0.4.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"
@@ -235,7 +235,7 @@
235
235
  "type": "string"
236
236
  },
237
237
  "python": {
238
- "default": "3.13",
238
+ "default": "3.12",
239
239
  "title": "Python",
240
240
  "type": "string"
241
241
  },
@@ -623,4 +623,4 @@ def test_state_size_outside_bounds_rejected(manifest_data, bad):
623
623
 
624
624
  def test_runtime_python_defaults_to_313(manifest_data):
625
625
  del manifest_data["runtime"]["python"]
626
- assert validate_manifest(manifest_data).runtime.python == "3.13"
626
+ assert validate_manifest(manifest_data).runtime.python == "3.12"
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "gravity-cli"
3
- version = "0.3.1"
3
+ version = "0.4.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.3.1", # released in lockstep: always the same version as this package
10
+ "gravity-schema==0.4.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",
@@ -23,8 +23,11 @@ def _token() -> str:
23
23
  return token
24
24
 
25
25
 
26
- def _request(method: str, path: str, *, auth: bool = True, timeout: int = 60, headers: dict | None = None, retry: bool = True, **kw) -> Any:
27
- """The JSON body. A refused connection or an error status becomes a Problem."""
26
+ def _request(
27
+ method: str, path: str, *, auth: bool = True, timeout: int = 60, headers: dict | None = None, retry: bool = True,
28
+ missing_ok: bool = False, **kw,
29
+ ) -> Any:
30
+ """The JSON body. A refused connection or an error status becomes a Problem; with missing_ok, a 404 is None."""
28
31
  extra = headers or {}
29
32
  sent = {**({"Authorization": f"Bearer {_token()}"} if auth else {}), **extra}
30
33
  try:
@@ -39,6 +42,8 @@ def _request(method: str, path: str, *, auth: bool = True, timeout: int = 60, he
39
42
  if retry and _refresh():
40
43
  return _request(method, path, auth=auth, timeout=timeout, headers=extra, retry=False, **kw)
41
44
  raise Problem("Your sign-in has expired", fix="Run gravity login again.")
45
+ if resp.status_code == 404 and missing_ok:
46
+ return None
42
47
  if resp.status_code >= 400:
43
48
  try:
44
49
  body = resp.json()
@@ -94,6 +99,11 @@ def get(path: str) -> Any:
94
99
  return _request("GET", path)
95
100
 
96
101
 
102
+ def gateway_token() -> dict | None:
103
+ """A gateway token for this builder: token, expires_at, budget_usd. None until Gravity issues them."""
104
+ return _request("POST", "/api/cli/gateway-token", missing_ok=True, json={})
105
+
106
+
97
107
  def push_agent(manifest_json: dict, zip_bytes: bytes, source_sha256: str) -> dict:
98
108
  headers = {
99
109
  "Content-Type": "application/octet-stream",
@@ -14,6 +14,7 @@ import typer
14
14
 
15
15
  from .. import Problem, project, ui
16
16
  from ..manifest import ManifestValidationError, load_manifest
17
+ from .login import gateway_token
17
18
  from .run import BUILDER_TOKENS, _mint_run_token, gateway_urls
18
19
 
19
20
  ATTEMPTS = 3
@@ -93,7 +94,7 @@ def adapt_command(
93
94
  ui.ok("Declared in manifest.yaml")
94
95
 
95
96
  _, gateway_base = gateway_urls()
96
- dev = {"Authorization": f"Bearer {os.environ.get('GRAVITY_DEV_TOKEN', '')}"}
97
+ dev = {"Authorization": f"Bearer {gateway_token()}"}
97
98
  all_tools = sorted({t for tl in manifest.slots[slot].adapters.values() for t in tl} | set(grant))
98
99
  try:
99
100
  schemas = httpx.get(f"{gateway_base}/tools", params={"names": ",".join(all_tools)}, headers=dev, timeout=15)
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+ import webbrowser
6
+ from datetime import UTC, datetime
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+ from .. import Problem, api, config, ui
12
+
13
+
14
+ def login_command():
15
+ """Log in with a browser device code; the token goes in the OS keychain."""
16
+ start = api.device_start()
17
+ device_code = start["deviceCode"]
18
+ interval = start.get("interval", 3)
19
+ deadline = time.time() + start.get("expiresIn", 600)
20
+
21
+ ui.heading("Sign in to Gravity")
22
+ ui.table(["", "", ""], [
23
+ ["1", "Your browser is opening", start["verificationUrl"]],
24
+ ["2", "Confirm this code", start["userCode"]],
25
+ ])
26
+ ui.say()
27
+ webbrowser.open(start["verificationUrl"])
28
+
29
+ with ui.working("Waiting for you to approve") as show:
30
+ while (left := deadline - time.time()) > 0:
31
+ show(f"Waiting for you to approve {int(left) // 60}:{int(left) % 60:02d} left")
32
+ time.sleep(interval)
33
+ result = api.device_poll(device_code)
34
+ if result["status"] == "approved":
35
+ session = result["session"]
36
+ config.save_session(config.Session(session["accessToken"], session["refreshToken"]))
37
+ break
38
+ else:
39
+ raise Problem("Sign-in timed out", fix="Run gravity login again.")
40
+ ui.ok("Signed in")
41
+ issued = api.gateway_token()
42
+ if issued:
43
+ config.save_session(config.Session(session["accessToken"], session["refreshToken"], issued))
44
+ ui.ok("Gateway token", _token_status(issued))
45
+
46
+
47
+ def _token_status(gateway: dict) -> str:
48
+ until = f"valid until {datetime.fromisoformat(gateway['expires_at']).astimezone():%a %d %b %H:%M}" if gateway.get("expires_at") else "does not expire"
49
+ budget = f" · budget ${gateway['budget_usd']:.2f}" if gateway.get("budget_usd") is not None else ""
50
+ return until + budget
51
+
52
+
53
+ def gateway_token() -> str:
54
+ """The token gravity run uses: GRAVITY_DEV_TOKEN if set, else the one gravity login stored."""
55
+ if token := os.environ.get("GRAVITY_DEV_TOKEN"):
56
+ return token
57
+ session = config.load_session()
58
+ if not (session and session.gateway):
59
+ raise Problem("No gateway token", fix="Run gravity login, or add GRAVITY_DEV_TOKEN to ~/.gravity/.env")
60
+ expires = session.gateway.get("expires_at")
61
+ if expires and datetime.fromisoformat(expires) <= datetime.now(UTC):
62
+ raise Problem("Your gateway token has expired", fix="Run gravity login again.")
63
+ return session.gateway["token"]
64
+
65
+
66
+ def whoami_command(
67
+ token: Annotated[bool, typer.Option("--token", help="Print the gateway token itself, for CI or a teammate's .env")] = False,
68
+ ):
69
+ """If you forget who you are."""
70
+ if token:
71
+ ui.out.print(gateway_token(), markup=False) # plain, so it can be piped or copied
72
+ return
73
+ me = api.get("/api/cli/me")
74
+ me = me if isinstance(me, dict) else {}
75
+ # Only the dev token (GRAVITY_API_TOKEN) is a user without an email.
76
+ who = me.get("email") or f"{me.get('id', '')} (dev token)"
77
+ ui.ok(who, me.get("userType", ""))
78
+ session = config.load_session()
79
+ if os.environ.get("GRAVITY_DEV_TOKEN"):
80
+ ui.ok("Gateway token", "from GRAVITY_DEV_TOKEN in your environment")
81
+ elif session and session.gateway:
82
+ ui.ok("Gateway token", _token_status(session.gateway))
83
+ else:
84
+ ui.warn("No gateway token", "gravity run needs one.", "Run gravity login again, or set GRAVITY_DEV_TOKEN.")
85
+
86
+
87
+ def logout_command():
88
+ config.clear_session()
89
+ ui.ok("Signed out")
@@ -15,6 +15,7 @@ import typer
15
15
  from .. import Problem, api, project, ui
16
16
  from ..manifest import load_manifest
17
17
  from ..timeline import EVENT_PREFIX, Timeline
18
+ from .login import gateway_token
18
19
 
19
20
  RUNNER_SCRIPT = Path(__file__).parent.parent / "_runner_script.py"
20
21
  # The builder's own tokens: never handed to agent or LLM-written code.
@@ -41,9 +42,7 @@ def _check_version(minimum: str | None) -> None:
41
42
 
42
43
  def _mint_run_token(gateway_base: str, tools: list[str], approvals: list[str] = ()) -> dict:
43
44
  """Dev token -> a run token scoped to the manifest's tools, as a hosted run gets."""
44
- dev_token = os.environ.get("GRAVITY_DEV_TOKEN")
45
- if not dev_token:
46
- raise Problem("No gateway dev token", fix="Add GRAVITY_DEV_TOKEN to ~/.gravity/.env")
45
+ dev_token = gateway_token()
47
46
  try:
48
47
  resp = httpx.post(
49
48
  f"{gateway_base}/runs",
@@ -89,7 +88,7 @@ def _summary(arguments: dict) -> str:
89
88
 
90
89
  def _ask_approval(live, timeline: Timeline, ev: dict, gateway_base: str, run_id: str | None) -> None:
91
90
  """Find the held call, show what it would do, send the builder's answer."""
92
- headers = {"Authorization": f"Bearer {os.environ['GRAVITY_DEV_TOKEN']}"}
91
+ headers = {"Authorization": f"Bearer {gateway_token()}"}
93
92
  name = timeline._name(ev["tool"])
94
93
  pending = None
95
94
  for _ in range(60): # the call reaches the gateway a moment after it starts
@@ -156,7 +155,7 @@ def run_command(
156
155
  None, "--reply", help='Continue the last conversation of a chat agent, e.g. --reply "make it shorter"'
157
156
  ),
158
157
  ):
159
- """Run your agent locally through the gateway. Needs GRAVITY_DEV_TOKEN in ~/.gravity/.env."""
158
+ """Run your agent locally through the gateway, with the token gravity login stored or GRAVITY_DEV_TOKEN."""
160
159
  project_dir = path.resolve()
161
160
  manifest = load_manifest(project_dir / "manifest.yaml")
162
161
 
@@ -8,7 +8,7 @@ import time
8
8
  from pathlib import Path
9
9
 
10
10
  import typer
11
- from gravity_schema import DEV_TOOL_NAMES, LOCKFILE_PLATFORM
11
+ from gravity_schema import DEV_TOOL_NAMES, LOCKFILE_PLATFORM, PLATFORM_PYTHON
12
12
  from gravity_schema.contract import TOOL_NAME_RE as _TOOL_NAME_RE
13
13
 
14
14
  from .. import ui
@@ -156,6 +156,11 @@ def check(project_dir: Path) -> tuple[Manifest, int, int]:
156
156
  ui.ok("Entrypoint", manifest.runtime.entrypoint)
157
157
  problems += len(findings)
158
158
 
159
+ if manifest.runtime.python != PLATFORM_PYTHON: # the hosted build refuses it, so fail here instead
160
+ ui.bad("Python", f"manifest declares {manifest.runtime.python} but the platform runs {PLATFORM_PYTHON}",
161
+ f'Set runtime.python to "{PLATFORM_PYTHON}" in {MANIFEST_FILE}.')
162
+ problems += 1
163
+
159
164
  lock = manifest.dependencies.lockfile
160
165
  target = f"{LOCKFILE_PLATFORM} · py{manifest.runtime.python}"
161
166
  if not (project_dir / "requirements.txt").is_file():
@@ -57,6 +57,7 @@ def first_run() -> bool:
57
57
  class Session:
58
58
  access_token: str
59
59
  refresh_token: str
60
+ gateway: dict | None = None # {"token", "expires_at", "budget_usd"}, once Gravity issues per-builder tokens
60
61
 
61
62
 
62
63
  # Keychain calls fail on headless/CI boxes with no backend; the 0600 file covers those.
@@ -79,7 +80,7 @@ def load_session() -> Session | None:
79
80
  return None
80
81
  try:
81
82
  data = json.loads(payload)
82
- return Session(data["access_token"], data["refresh_token"])
83
+ return Session(data["access_token"], data["refresh_token"], data.get("gateway"))
83
84
  except (json.JSONDecodeError, KeyError, TypeError):
84
85
  return None
85
86
 
@@ -5,7 +5,7 @@ description: "a conversational langgraph agent - keeps the thread, so you can an
5
5
 
6
6
  runtime:
7
7
  framework: langgraph
8
- python: "3.13"
8
+ python: "3.12"
9
9
  entrypoint: src/agent.py:graph
10
10
  timeout_seconds: 300
11
11
 
@@ -5,7 +5,7 @@ description: "a deepagents-harness agent, scaffolded by gravity init"
5
5
 
6
6
  runtime:
7
7
  framework: langgraph # deepagents compiles to a langgraph graph
8
- python: "3.13"
8
+ python: "3.12"
9
9
  entrypoint: src/agent.py:graph
10
10
  timeout_seconds: 300
11
11
 
@@ -5,7 +5,7 @@ description: "a minimal langgraph agent, scaffolded by gravity init"
5
5
 
6
6
  runtime:
7
7
  framework: langgraph # v1: langgraph only
8
- python: "3.13"
8
+ python: "3.12"
9
9
  entrypoint: src/agent.py:graph # file:object — the graph the runner invokes
10
10
  timeout_seconds: 300
11
11
 
@@ -5,7 +5,7 @@ description: "a structured langgraph agent, scaffolded by gravity init"
5
5
 
6
6
  runtime:
7
7
  framework: langgraph
8
- python: "3.13"
8
+ python: "3.12"
9
9
  entrypoint: src/agent.py:graph
10
10
  timeout_seconds: 300
11
11
 
@@ -142,3 +142,14 @@ def test_lock_is_compiled_with_paths_relative_to_the_project(project, monkeypatc
142
142
  assert Path(cwd) == project
143
143
  assert str(project) not in " ".join(cmd), cmd
144
144
  assert cmd[-3:] == ["requirements.txt", "-o", "requirements.lock"]
145
+
146
+
147
+ def test_validate_fails_on_a_python_the_platform_does_not_run(project, capsys):
148
+ """The hosted build refuses any Python but the platform's; the push failed minutes
149
+ after it was accepted. validate says so before the push."""
150
+ from gravity_cli.commands import validate
151
+
152
+ (project / "manifest.yaml").write_text(MANIFEST.replace('python: "3.12"', 'python: "3.13"'))
153
+ with pytest.raises(typer.Exit):
154
+ validate.validate_command(project)
155
+ assert "platform runs 3.12" in capsys.readouterr().out
@@ -0,0 +1,72 @@
1
+ """The per-builder gateway token: login stores it, run uses it, whoami shows its status."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime, timedelta
6
+
7
+ import pytest
8
+ import typer
9
+
10
+ from gravity_cli import config
11
+ from gravity_cli.commands import login
12
+
13
+ SOON = (datetime.now(UTC) + timedelta(hours=12)).isoformat().replace("+00:00", "Z")
14
+ ISSUED = {"token": "grt_builder", "token_id": "t1", "expires_at": SOON, "budget_usd": 5.0}
15
+
16
+
17
+ @pytest.fixture
18
+ def stored(monkeypatch):
19
+ """The keychain, as a list whose last item is the current session."""
20
+ sessions: list = []
21
+ monkeypatch.setattr(config, "save_session", sessions.append)
22
+ monkeypatch.setattr(config, "load_session", lambda: sessions[-1] if sessions else None)
23
+ monkeypatch.delenv("GRAVITY_DEV_TOKEN", raising=False)
24
+ return sessions
25
+
26
+
27
+ def _login(monkeypatch, issued):
28
+ monkeypatch.setattr(login.api, "device_start", lambda: {"deviceCode": "d", "userCode": "U", "verificationUrl": "https://x", "interval": 0})
29
+ monkeypatch.setattr(login.api, "device_poll", lambda code: {"status": "approved", "session": {"accessToken": "a", "refreshToken": "r"}})
30
+ monkeypatch.setattr(login.api, "gateway_token", lambda: issued)
31
+ monkeypatch.setattr(login.webbrowser, "open", lambda url: None)
32
+ login.login_command()
33
+
34
+
35
+ def test_login_stores_the_gateway_token_and_run_uses_it(stored, monkeypatch, capsys):
36
+ _login(monkeypatch, ISSUED)
37
+ assert stored[-1].gateway == ISSUED
38
+ assert "Gateway token" in capsys.readouterr().out
39
+ assert login.gateway_token() == "grt_builder"
40
+
41
+
42
+ def test_login_still_works_before_gravity_issues_tokens(stored, monkeypatch):
43
+ _login(monkeypatch, None) # the endpoint answered 404
44
+ assert stored[-1].gateway is None
45
+ with pytest.raises(typer.Exit) as e:
46
+ login.gateway_token()
47
+ assert "gravity login" in str(e.value) and "GRAVITY_DEV_TOKEN" in str(e.value)
48
+
49
+
50
+ def test_the_env_token_wins_and_an_expired_stored_one_is_refused(stored, monkeypatch):
51
+ stored.append(config.Session("a", "r", {**ISSUED, "expires_at": "2020-01-01T00:00:00Z"}))
52
+ with pytest.raises(typer.Exit) as e:
53
+ login.gateway_token()
54
+ assert "expired" in str(e.value)
55
+ monkeypatch.setenv("GRAVITY_DEV_TOKEN", "grt_env")
56
+ assert login.gateway_token() == "grt_env"
57
+
58
+
59
+ def test_a_token_without_an_expiry_is_fine(stored, capsys):
60
+ stored.append(config.Session("a", "r", {"token": "grt_forever", "expires_at": None, "budget_usd": None}))
61
+ assert login.gateway_token() == "grt_forever"
62
+ assert "does not expire" in login._token_status(stored[-1].gateway)
63
+
64
+
65
+ def test_whoami_shows_token_status_and_prints_it_only_on_request(stored, monkeypatch, capsys):
66
+ stored.append(config.Session("a", "r", ISSUED))
67
+ monkeypatch.setattr(login.api, "get", lambda path: {"email": "me@x", "userType": "creator"})
68
+ login.whoami_command(token=False)
69
+ out = capsys.readouterr().out
70
+ assert "valid until" in out and "budget $5.00" in out and "grt_builder" not in out
71
+ login.whoami_command(token=True)
72
+ assert capsys.readouterr().out.strip() == "grt_builder"
@@ -38,7 +38,7 @@ def test_manifest_is_valid_and_targets_313(tmp_path, template):
38
38
 
39
39
  manifest = load_manifest(path)
40
40
 
41
- assert manifest.runtime.python == "3.13"
41
+ assert manifest.runtime.python == "3.12"
42
42
  assert manifest.runtime.entrypoint == "src/agent.py:graph"
43
43
 
44
44
 
@@ -59,7 +59,7 @@ name = "cffi"
59
59
  version = "2.1.1"
60
60
  source = { registry = "https://pypi.org/simple" }
61
61
  dependencies = [
62
- { name = "pycparser", marker = "implementation_name != 'PyPy'" },
62
+ { name = "pycparser", marker = "implementation_name != 'PyPy' and sys_platform != 'win32'" },
63
63
  ]
64
64
  sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
65
65
  wheels = [
@@ -123,7 +123,7 @@ name = "cryptography"
123
123
  version = "50.0.0"
124
124
  source = { registry = "https://pypi.org/simple" }
125
125
  dependencies = [
126
- { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
126
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'win32'" },
127
127
  ]
128
128
  sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
129
129
  wheels = [
@@ -164,7 +164,7 @@ wheels = [
164
164
 
165
165
  [[package]]
166
166
  name = "gravity-cli"
167
- version = "0.3.1"
167
+ version = "0.4.0"
168
168
  source = { editable = "." }
169
169
  dependencies = [
170
170
  { name = "gravity-schema" },
@@ -201,7 +201,7 @@ dev = [
201
201
 
202
202
  [[package]]
203
203
  name = "gravity-schema"
204
- version = "0.3.1"
204
+ version = "0.4.0"
205
205
  source = { editable = "packages/gravity-schema" }
206
206
  dependencies = [
207
207
  { name = "pydantic" },
@@ -619,8 +619,8 @@ name = "secretstorage"
619
619
  version = "3.5.0"
620
620
  source = { registry = "https://pypi.org/simple" }
621
621
  dependencies = [
622
- { name = "cryptography" },
623
- { name = "jeepney" },
622
+ { name = "cryptography", marker = "sys_platform != 'win32'" },
623
+ { name = "jeepney", marker = "sys_platform != 'win32'" },
624
624
  ]
625
625
  sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
626
626
  wheels = [
@@ -1,49 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import time
4
- import webbrowser
5
-
6
- from .. import Problem, api, config, ui
7
-
8
-
9
- def login_command():
10
- """Log in with a browser device code; the token goes in the OS keychain."""
11
- start = api.device_start()
12
- device_code = start["deviceCode"]
13
- interval = start.get("interval", 3)
14
- deadline = time.time() + start.get("expiresIn", 600)
15
-
16
- ui.heading("Sign in to Gravity")
17
- ui.table(["", "", ""], [
18
- ["1", "Your browser is opening", start["verificationUrl"]],
19
- ["2", "Confirm this code", start["userCode"]],
20
- ])
21
- ui.say()
22
- webbrowser.open(start["verificationUrl"])
23
-
24
- with ui.working("Waiting for you to approve") as show:
25
- while (left := deadline - time.time()) > 0:
26
- show(f"Waiting for you to approve {int(left) // 60}:{int(left) % 60:02d} left")
27
- time.sleep(interval)
28
- result = api.device_poll(device_code)
29
- if result["status"] == "approved":
30
- session = result["session"]
31
- config.save_session(config.Session(session["accessToken"], session["refreshToken"]))
32
- break
33
- else:
34
- raise Problem("Sign-in timed out", fix="Run gravity login again.")
35
- ui.ok("Signed in")
36
-
37
-
38
- def whoami_command():
39
- """If you forget who you are."""
40
- me = api.get("/api/cli/me")
41
- me = me if isinstance(me, dict) else {}
42
- # Only the dev token (GRAVITY_API_TOKEN) is a user without an email.
43
- who = me.get("email") or f"{me.get('id', '')} (dev token)"
44
- ui.ok(who, me.get("userType", ""))
45
-
46
-
47
- def logout_command():
48
- config.clear_session()
49
- ui.ok("Signed out")
File without changes
File without changes
File without changes
File without changes