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.
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/.env.example +3 -3
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/PKG-INFO +17 -15
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/README.md +15 -13
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/__init__.py +2 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/contract.py +3 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/manifest.py +1 -1
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/pyproject.toml +1 -1
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/schema/manifest.schema.json +1 -1
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_manifest.py +1 -1
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/pyproject.toml +2 -2
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/api.py +12 -2
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/adapt.py +2 -1
- gravity_cli-0.4.0/src/gravity_cli/commands/login.py +89 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/run.py +4 -5
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/validate.py +6 -1
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/config.py +2 -1
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/chat/manifest.yaml +1 -1
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/deepagent/manifest.yaml +1 -1
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/minimal/manifest.yaml +1 -1
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/manifest.yaml +1 -1
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_build_push.py +11 -0
- gravity_cli-0.4.0/tests/test_gateway_token.py +72 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_templates.py +1 -1
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/uv.lock +6 -6
- gravity_cli-0.3.1/src/gravity_cli/commands/login.py +0 -49
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/.github/workflows/ci.yml +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/.github/workflows/release.yml +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/.gitignore +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/AGENT_STANDARD.md +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/LICENSE +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/NOTICE +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/TEMPLATES.md +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/LICENSE +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/NOTICE +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/README.md +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/__main__.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/catalog.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/catalog.yaml +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/catalog_imported.yaml +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/cron.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/errors.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/export.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/gen_catalog.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/validation.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/conftest.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_catalog_parity.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_cron.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_errors.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_json_schema.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_kind_outputs.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_slots.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_validation.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/BUILDER_GUIDE.md +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/__init__.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/__main__.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/_runner_script.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/cli.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/__init__.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/build.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/guide.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/init.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/push.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/runs.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/schedule.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/commands/tools.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/manifest.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/project.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/chat/.gitignore +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/chat/requirements.txt +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/chat/src/agent.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/deepagent/.gitignore +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/deepagent/requirements.txt +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/deepagent/src/agent.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/minimal/.gitignore +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/minimal/requirements.txt +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/minimal/src/agent.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/.gitignore +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/requirements.txt +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/src/agent.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/src/gateway.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/src/nodes/agent.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/src/prompts.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/src/state.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/timeline.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/ui.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/starter/manifest.yaml +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/starter/requirements.txt +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/starter/src/agent.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/starter/src/summary.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/starter/tests/test_summary.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/conftest.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_adapt.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_api.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_manifest_integration.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_run.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_timeline.py +0 -0
- {gravity_cli-0.3.1 → gravity_cli-0.4.0}/tests/test_ui.py +0 -0
- {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
|
-
#
|
|
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
|
+
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.
|
|
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
|
|
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.
|
|
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 `
|
|
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=<
|
|
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` |
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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 `
|
|
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=<
|
|
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` |
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
|
|
@@ -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.
|
|
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
|
+
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.
|
|
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(
|
|
27
|
-
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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
|
|
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
|
|
|
@@ -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"
|
|
@@ -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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/catalog_imported.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/gen_catalog.py
RENAMED
|
File without changes
|
{gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/gravity_schema/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
{gravity_cli-0.3.1 → gravity_cli-0.4.0}/packages/gravity-schema/tests/test_catalog_parity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/deepagent/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.3.1 → gravity_cli-0.4.0}/src/gravity_cli/templates/structured/src/nodes/agent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|