gravity-cli 0.7.2__tar.gz → 0.7.4__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.7.2 → gravity_cli-0.7.4}/PKG-INFO +2 -2
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/catalog.yaml +27 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/contract.py +4 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/errors.py +1 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/manifest.py +63 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/pyproject.toml +1 -1
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/schema/manifest.schema.json +46 -0
- gravity_cli-0.7.4/packages/gravity-schema/tests/test_accounts.py +42 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/pyproject.toml +2 -2
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/CHANGELOG.md +9 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/push.py +3 -1
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/docs/Gravity.agent.md +15 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_build_push.py +17 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/uv.lock +2 -2
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/.env.example +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/.github/workflows/ci.yml +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/.github/workflows/release.yml +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/.gitignore +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/AGENT_STANDARD.md +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/LICENSE +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/NOTICE +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/README.md +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/RELEASING.md +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/TEMPLATES.md +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/LICENSE +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/NOTICE +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/README.md +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/__init__.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/__main__.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/catalog.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/catalog_imported.yaml +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/cron.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/export.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/gen_catalog.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/rules.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/rules_cases.json +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/source_scan.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/validation.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/conftest.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_actions_memory.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_catalog_parity.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_cron.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_errors.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_json_schema.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_kind_outputs.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_manifest.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_rules.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_slots.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_source_scan.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_validation.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/BUILDER_GUIDE.md +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/__init__.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/__main__.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/_runner_script.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/api.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/changelog.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/cli.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/__init__.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/adapt.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/build.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/changelog.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/connect.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/dev.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/docs.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/guide.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/init.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/login.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/memory.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/run.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/runs.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/schedule.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/tools.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/update.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/commands/validate.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/config.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/dev.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/dev_page/index.html +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/docs/problem-to-agent.md +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/inputs.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/localrun.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/manifest.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/project.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/chat/.gitignore +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/chat/manifest.yaml +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/chat/requirements.txt +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/chat/src/agent.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/deepagent/.gitignore +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/deepagent/manifest.yaml +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/deepagent/requirements.txt +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/deepagent/src/agent.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/minimal/.gitignore +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/minimal/manifest.yaml +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/minimal/requirements.txt +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/minimal/src/agent.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/structured/.gitignore +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/structured/manifest.yaml +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/structured/requirements.txt +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/structured/src/agent.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/structured/src/gateway.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/structured/src/nodes/agent.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/structured/src/prompts.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/structured/src/state.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/timeline.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/ui.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/updater.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/workspace.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/starter/manifest.yaml +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/starter/requirements.txt +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/starter/src/agent.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/starter/src/summary.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/starter/tests/test_summary.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/conftest.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_adapt.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_agent_guide.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_agents.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_api.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_changelog.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_connect.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_dev.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_gateway_token.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_inputs_memory.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_localrun.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_manifest_integration.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_run.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_runs.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_schedule.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_session.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_templates.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_timeline.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_tools.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_ui.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_updater.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_validate.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_welcome.py +0 -0
- {gravity_cli-0.7.2 → gravity_cli-0.7.4}/tests/test_workspace.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gravity-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
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.7.
|
|
9
|
+
Requires-Dist: gravity-schema==0.7.4
|
|
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
|
|
@@ -65,6 +65,24 @@
|
|
|
65
65
|
guarantee: certified
|
|
66
66
|
side_effect: read
|
|
67
67
|
|
|
68
|
+
- name: browser.read
|
|
69
|
+
slug: null
|
|
70
|
+
toolkit: null
|
|
71
|
+
description: read a page of a site the person signed in to once (an account the manifest declares) through Gravity's browser worker, as that person; returns the final url, the page text and its links. Only pages of the account's own site. The agent never sees the login. Charged to the run's budget
|
|
72
|
+
scopes: []
|
|
73
|
+
breadth: generic
|
|
74
|
+
guarantee: certified
|
|
75
|
+
side_effect: read
|
|
76
|
+
|
|
77
|
+
- name: browser.act
|
|
78
|
+
slug: null
|
|
79
|
+
toolkit: null
|
|
80
|
+
description: do one typed action as the person on a site account they signed in to (linkedin.com send_dm, connect, comment; x.com send_dm, reply), with the text given; paced and capped per account per day by the gateway. Must be under approvals.required_for. Returns what was done, or a refusal code (NOT_SIGNED_IN, NOT_CONNECTED, DAILY_CAP, BLOCKED_BY_SITE)
|
|
81
|
+
scopes: []
|
|
82
|
+
breadth: generic
|
|
83
|
+
guarantee: certified
|
|
84
|
+
side_effect: write
|
|
85
|
+
|
|
68
86
|
- name: supadata.get_transcript
|
|
69
87
|
slug: null
|
|
70
88
|
toolkit: null
|
|
@@ -354,6 +372,15 @@
|
|
|
354
372
|
guarantee: certified
|
|
355
373
|
side_effect: read
|
|
356
374
|
|
|
375
|
+
- name: apollo.enrich_person
|
|
376
|
+
slug: APOLLO_PEOPLE_ENRICHMENT
|
|
377
|
+
toolkit: apollo
|
|
378
|
+
description: match one person in Apollo from their name and company domain (or LinkedIn URL) and return their work email, title and profile links; uses the person's own Apollo account and its credits
|
|
379
|
+
scopes: []
|
|
380
|
+
breadth: generic
|
|
381
|
+
guarantee: community
|
|
382
|
+
side_effect: read
|
|
383
|
+
|
|
357
384
|
- name: hunter.find_email
|
|
358
385
|
slug: HUNTER_EMAIL_FINDER
|
|
359
386
|
toolkit: hunter
|
|
@@ -82,6 +82,10 @@ MAX_PREVIOUS_KB: Final[int] = 256
|
|
|
82
82
|
|
|
83
83
|
# Declared actions (manifest `actions`).
|
|
84
84
|
MAX_ACTIONS: Final[int] = 20
|
|
85
|
+
# Sites the gateway's browser worker has sign-in and recipes for (accounts, browser.read, browser.act).
|
|
86
|
+
ACCOUNT_SITES: Final[frozenset[str]] = frozenset({"linkedin.com", "x.com"})
|
|
87
|
+
MAX_ACCOUNTS: Final[int] = 5
|
|
88
|
+
BROWSER_TOOLS: Final[frozenset[str]] = frozenset({"browser.read", "browser.act"})
|
|
85
89
|
|
|
86
90
|
|
|
87
91
|
# Generous bounds, so a pathological manifest can't tie up the validator.
|
|
@@ -39,6 +39,7 @@ class ErrorCode(str, Enum):
|
|
|
39
39
|
INVALID_CONDITION = "INVALID_CONDITION"
|
|
40
40
|
INVALID_ACTION = "INVALID_ACTION"
|
|
41
41
|
INVALID_MEMORY = "INVALID_MEMORY"
|
|
42
|
+
INVALID_ACCOUNT = "INVALID_ACCOUNT"
|
|
42
43
|
LOCKFILE_MISMATCH = "LOCKFILE_MISMATCH"
|
|
43
44
|
MISSING_FIELD = "MISSING_FIELD"
|
|
44
45
|
UNKNOWN_FIELD = "UNKNOWN_FIELD"
|
|
@@ -726,6 +726,42 @@ class ActionSpec(BaseModel):
|
|
|
726
726
|
return self
|
|
727
727
|
|
|
728
728
|
|
|
729
|
+
class AccountSpec(BaseModel):
|
|
730
|
+
"""A site account the person signs in to once on a Gravity page (Accounts vault). The agent never sees the
|
|
731
|
+
login: the gateway's browser worker opens the session for browser.read and browser.act calls only."""
|
|
732
|
+
|
|
733
|
+
model_config = _STRICT
|
|
734
|
+
|
|
735
|
+
name: Annotated[str, Field(max_length=C.MAX_NAME_LENGTH)]
|
|
736
|
+
site: str
|
|
737
|
+
label: Annotated[str, Field(max_length=C.MAX_LABEL_LENGTH)] = ""
|
|
738
|
+
purpose: Annotated[str, Field(max_length=C.MAX_LABEL_LENGTH)] = ""
|
|
739
|
+
required: bool = False
|
|
740
|
+
|
|
741
|
+
@field_validator("name")
|
|
742
|
+
@classmethod
|
|
743
|
+
def _check_name(cls, v: str) -> str:
|
|
744
|
+
if not C.INPUT_NAME_RE.match(v):
|
|
745
|
+
raise PydanticCustomError(
|
|
746
|
+
ErrorCode.INVALID_ACCOUNT.value,
|
|
747
|
+
"Account name '{value}' is not a valid identifier.",
|
|
748
|
+
{"value": v, "remediation": "Use lowercase letters, digits and underscores, e.g. linkedin."},
|
|
749
|
+
)
|
|
750
|
+
return v
|
|
751
|
+
|
|
752
|
+
@field_validator("site")
|
|
753
|
+
@classmethod
|
|
754
|
+
def _check_site(cls, v: str) -> str:
|
|
755
|
+
site = v.strip().lower().removeprefix("www.")
|
|
756
|
+
if site not in C.ACCOUNT_SITES:
|
|
757
|
+
raise PydanticCustomError(
|
|
758
|
+
ErrorCode.INVALID_ACCOUNT.value,
|
|
759
|
+
"Accounts on '{value}' are not supported.",
|
|
760
|
+
{"value": v, "remediation": f"Use one of: {', '.join(sorted(C.ACCOUNT_SITES))}."},
|
|
761
|
+
)
|
|
762
|
+
return site
|
|
763
|
+
|
|
764
|
+
|
|
729
765
|
class OfferAs(str, Enum):
|
|
730
766
|
"""How the actions under a result are shown."""
|
|
731
767
|
|
|
@@ -767,6 +803,8 @@ class Manifest(BaseModel):
|
|
|
767
803
|
actions: Annotated[list[ActionSpec], Field(max_length=C.MAX_ACTIONS)] = Field(default_factory=list)
|
|
768
804
|
offer_as: OfferAs = OfferAs.BUTTONS
|
|
769
805
|
offer_label: Annotated[str, Field(max_length=C.MAX_LABEL_LENGTH)] | None = None
|
|
806
|
+
# Site accounts the person signs in to once (Accounts vault); browser.* tools act through them.
|
|
807
|
+
accounts: Annotated[list[AccountSpec], Field(max_length=C.MAX_ACCOUNTS)] = Field(default_factory=list)
|
|
770
808
|
|
|
771
809
|
@field_validator("contract")
|
|
772
810
|
@classmethod
|
|
@@ -897,6 +935,31 @@ class Manifest(BaseModel):
|
|
|
897
935
|
)
|
|
898
936
|
return self
|
|
899
937
|
|
|
938
|
+
@model_validator(mode="after")
|
|
939
|
+
def _check_accounts(self) -> Manifest:
|
|
940
|
+
names = [a.name for a in self.accounts]
|
|
941
|
+
dupes = sorted({n for n in names if names.count(n) > 1})
|
|
942
|
+
if dupes:
|
|
943
|
+
raise PydanticCustomError(
|
|
944
|
+
ErrorCode.INVALID_ACCOUNT.value,
|
|
945
|
+
"Account name(s) declared twice: {value}.",
|
|
946
|
+
{"value": ", ".join(dupes), "remediation": "Give each account its own name."},
|
|
947
|
+
)
|
|
948
|
+
browser = sorted(self.declared_tools & C.BROWSER_TOOLS)
|
|
949
|
+
if browser and not self.accounts:
|
|
950
|
+
raise PydanticCustomError(
|
|
951
|
+
ErrorCode.INVALID_ACCOUNT.value,
|
|
952
|
+
"{value} acts through a person's own site account, and none is declared.",
|
|
953
|
+
{"value": ", ".join(browser), "remediation": "Declare the site under `accounts` (e.g. name: linkedin, site: linkedin.com)."},
|
|
954
|
+
)
|
|
955
|
+
if "browser.act" in self.declared_tools and "browser.act" not in self.approvals.required_for:
|
|
956
|
+
raise PydanticCustomError(
|
|
957
|
+
ErrorCode.APPROVAL_REQUIRED.value,
|
|
958
|
+
"browser.act sends messages as the person, so each call needs their approval.",
|
|
959
|
+
{"remediation": "Add browser.act under approvals.required_for, with a fallback for when it isn't approved."},
|
|
960
|
+
)
|
|
961
|
+
return self
|
|
962
|
+
|
|
900
963
|
@model_validator(mode="after")
|
|
901
964
|
def _check_memory(self) -> Manifest:
|
|
902
965
|
if self.state is not None and self.memory is not None:
|
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$defs": {
|
|
3
|
+
"AccountSpec": {
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"description": "A site account the person signs in to once on a Gravity page (Accounts vault). The agent never sees the\nlogin: the gateway's browser worker opens the session for browser.read and browser.act calls only.",
|
|
6
|
+
"properties": {
|
|
7
|
+
"label": {
|
|
8
|
+
"default": "",
|
|
9
|
+
"maxLength": 200,
|
|
10
|
+
"title": "Label",
|
|
11
|
+
"type": "string"
|
|
12
|
+
},
|
|
13
|
+
"name": {
|
|
14
|
+
"maxLength": 64,
|
|
15
|
+
"title": "Name",
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"purpose": {
|
|
19
|
+
"default": "",
|
|
20
|
+
"maxLength": 200,
|
|
21
|
+
"title": "Purpose",
|
|
22
|
+
"type": "string"
|
|
23
|
+
},
|
|
24
|
+
"required": {
|
|
25
|
+
"default": false,
|
|
26
|
+
"title": "Required",
|
|
27
|
+
"type": "boolean"
|
|
28
|
+
},
|
|
29
|
+
"site": {
|
|
30
|
+
"title": "Site",
|
|
31
|
+
"type": "string"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"required": [
|
|
35
|
+
"name",
|
|
36
|
+
"site"
|
|
37
|
+
],
|
|
38
|
+
"title": "AccountSpec",
|
|
39
|
+
"type": "object"
|
|
40
|
+
},
|
|
3
41
|
"ActionSpec": {
|
|
4
42
|
"additionalProperties": false,
|
|
5
43
|
"description": "One thing the agent can do. The runner passes the chosen ones in state[\"actions\"].",
|
|
@@ -736,6 +774,14 @@
|
|
|
736
774
|
"additionalProperties": false,
|
|
737
775
|
"description": "What a builder declares about an agent: identity, entrypoint, consumer inputs, and the tools it is permitted to call. Contract version v1.",
|
|
738
776
|
"properties": {
|
|
777
|
+
"accounts": {
|
|
778
|
+
"items": {
|
|
779
|
+
"$ref": "#/$defs/AccountSpec"
|
|
780
|
+
},
|
|
781
|
+
"maxItems": 5,
|
|
782
|
+
"title": "Accounts",
|
|
783
|
+
"type": "array"
|
|
784
|
+
},
|
|
739
785
|
"actions": {
|
|
740
786
|
"items": {
|
|
741
787
|
"$ref": "#/$defs/ActionSpec"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Site accounts (the Accounts vault) and the browser tools that act through them."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from gravity_schema import dev_catalog, load_manifest_text, validate_manifest
|
|
5
|
+
from gravity_schema.errors import ManifestValidationError
|
|
6
|
+
|
|
7
|
+
BASE = {"name": "leads", "runtime": {"entrypoint": "src/agent.py:graph"}}
|
|
8
|
+
LINKEDIN = {"name": "linkedin", "site": "www.LinkedIn.com", "label": "Your LinkedIn", "purpose": "send messages you approve"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def codes(manifest):
|
|
12
|
+
with pytest.raises(ManifestValidationError) as e:
|
|
13
|
+
validate_manifest(manifest)
|
|
14
|
+
return {i.code.value for i in e.value.issues}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_accounts_with_browser_tools_validate():
|
|
18
|
+
m = validate_manifest({**BASE, "accounts": [LINKEDIN, {"name": "x", "site": "x.com"}],
|
|
19
|
+
"permissions": {"tools": ["browser.read", "browser.act"]},
|
|
20
|
+
"approvals": {"required_for": ["browser.act"]}})
|
|
21
|
+
assert [a.site for a in m.accounts] == ["linkedin.com", "x.com"] and not m.accounts[0].required
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_account_rules():
|
|
25
|
+
assert codes({**BASE, "accounts": [{"name": "fb", "site": "facebook.com"}]}) == {"INVALID_ACCOUNT"}
|
|
26
|
+
assert codes({**BASE, "accounts": [LINKEDIN, LINKEDIN]}) == {"INVALID_ACCOUNT"}
|
|
27
|
+
assert codes({**BASE, "accounts": [{"name": "Linked In", "site": "linkedin.com"}]}) == {"INVALID_ACCOUNT"}
|
|
28
|
+
assert codes({**BASE, "permissions": {"tools": ["browser.read"]}}) == {"INVALID_ACCOUNT"} # no account to act through
|
|
29
|
+
assert codes({**BASE, "accounts": [LINKEDIN], "permissions": {"tools": ["browser.act"]}}) == {"APPROVAL_REQUIRED"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_browser_and_apollo_tools_are_in_the_catalog():
|
|
33
|
+
names = dev_catalog().names()
|
|
34
|
+
assert {"browser.read", "browser.act", "apollo.enrich_person"} <= names
|
|
35
|
+
text = """
|
|
36
|
+
name: leads
|
|
37
|
+
runtime: {entrypoint: src/agent.py:graph}
|
|
38
|
+
accounts: [{name: linkedin, site: linkedin.com}]
|
|
39
|
+
permissions: {tools: [browser.read, browser.act, apollo.enrich_person]}
|
|
40
|
+
approvals: {required_for: [browser.act]}
|
|
41
|
+
"""
|
|
42
|
+
assert load_manifest_text(text, catalog=dev_catalog()).accounts[0].name == "linkedin"
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "gravity-cli"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.4"
|
|
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.7.
|
|
10
|
+
"gravity-schema==0.7.4", # 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",
|
|
@@ -4,6 +4,15 @@ Shown by `gravity update` and `gravity changelog`, and after an update. Each rel
|
|
|
4
4
|
then one bullet per change: `- new|improved|fixed: A one-line headline. The detail behind it.`
|
|
5
5
|
Write it for a builder, not for a maintainer. `tests/test_changelog.py` fails a release that has no entry.
|
|
6
6
|
|
|
7
|
+
## 0.7.4 - 2026-10-24
|
|
8
|
+
|
|
9
|
+
- new: Site accounts. Declare `accounts` (linkedin.com, x.com) and the person signs in once on Gravity; browser.read reads their pages and browser.act sends a DM, a connection note, a comment or a reply they approve, paced and capped per day. Agent code never sees the login.
|
|
10
|
+
- new: apollo.enrich_person matches a person in Apollo from their name and company domain and returns their work email and profile links, on the person's own Apollo account.
|
|
11
|
+
|
|
12
|
+
## 0.7.3 - 2026-10-23
|
|
13
|
+
|
|
14
|
+
- fixed: gravity push fits bigger agents. The manifest travels in a 12 KB request header and every unset field went up as a null, about 100 bytes per input and action; they are now left out, so an agent with a full form and many actions uploads.
|
|
15
|
+
|
|
7
16
|
## 0.7.2 - 2026-10-23
|
|
8
17
|
|
|
9
18
|
- new: Google Forms tools. forms.create, forms.update, forms.publish_settings, forms.get and forms.list_responses build a form in the person's own Google account, open it, close it at a deadline and read the responses. The API cannot make file-upload questions, so ask for a link instead.
|
|
@@ -50,7 +50,9 @@ def push_command(
|
|
|
50
50
|
ui.ok("Build is current", f"{zip_path.name} · {ui.size(len(zip_bytes))} · sha256 {zip_sha256[:12]}")
|
|
51
51
|
|
|
52
52
|
with ui.working("Uploading"):
|
|
53
|
-
|
|
53
|
+
# Unset fields are left out: as nulls they cost about 100 bytes per input and action against the 12 KB header,
|
|
54
|
+
# and the platform reads a missing field the same way.
|
|
55
|
+
result = api.push_agent(manifest.model_dump(mode="json", exclude_none=True), zip_bytes, zip_sha256)
|
|
54
56
|
name, version = result["name"], result["version"]
|
|
55
57
|
if result["status"] == "failed": # an unchanged re-push of a failed version: the platform does not rebuild it
|
|
56
58
|
v = api.get(f"/api/cli/agents/{name}/versions/{version}")
|
|
@@ -307,6 +307,9 @@ actions: # what the agent can do (section 10); leave o
|
|
|
307
307
|
offer_as: buttons # buttons (default) | question: how the actions show under the result
|
|
308
308
|
offer_label: "What next?"
|
|
309
309
|
|
|
310
|
+
# accounts: # a site account the person signs in to once on Gravity (section 6, browser.*)
|
|
311
|
+
# - { name: linkedin, site: linkedin.com, label: "Your LinkedIn", purpose: "send messages you approve" }
|
|
312
|
+
|
|
310
313
|
outputs: # what a person may ask the result to become (section 9), at most 8
|
|
311
314
|
summary: {renderer: markdown, label: "Summary", destinations: [download, gmail.draft, gmail.send]}
|
|
312
315
|
report: {renderer: docx, label: "Report (Word)"}
|
|
@@ -349,6 +352,7 @@ Rules the validator enforces beyond field types:
|
|
|
349
352
|
- **Actions** (`INVALID_ACTION`): names unique; `requires` and `then` name other declared actions and never loop;
|
|
350
353
|
at least one is `default: true`, and a default action's `requires` are defaults declared before it; a
|
|
351
354
|
`run_once` action has no `schedule` and is not a `then` target; an action's own inputs have no action-choice.
|
|
355
|
+
- **Accounts** (`INVALID_ACCOUNT`): sites are linkedin.com or x.com; names unique identifiers; `browser.read` or `browser.act` needs at least one account, and `browser.act` must be under `approvals.required_for` (`APPROVAL_REQUIRED`).
|
|
352
356
|
- **Memory** (`INVALID_MEMORY`): `memory` and `state` are not both declared; collection names are identifiers;
|
|
353
357
|
there is no shared-across-people collection.
|
|
354
358
|
- **Quote** `python: "3.12"` and write `version: 1.0.0` as a plain semver (`1.0` is a YAML float and is refused).
|
|
@@ -517,6 +521,16 @@ Catalog of this release: {{tool_counts}}.
|
|
|
517
521
|
an error, so check `status`. Each call costs about $0.0002 from the run's budget, and a run gets at most 200.
|
|
518
522
|
Private, internal and metadata addresses, ports other than 80/443/8080/8443, and non-http(s) URLs answer
|
|
519
523
|
`URL_REFUSED`. Prefer it over a `proxy_url` secret input: the person supplies nothing.
|
|
524
|
+
- `browser.read` and `browser.act` work as the person on a site they signed in to once on Gravity (My agents,
|
|
525
|
+
Accounts). Declare the site under `accounts` (`linkedin.com` or `x.com`); the person's login and session stay in
|
|
526
|
+
Gravity's vault and browser worker, and agent code never sees them. `browser.read {account, url}` returns `url`,
|
|
527
|
+
`text` and `links` for a page of that account's own site only. `browser.act {account, action, target, text}` does one
|
|
528
|
+
typed action: `send_dm` (a profile URL or handle; LinkedIn only to first-degree connections), `connect` (a LinkedIn
|
|
529
|
+
profile URL, note at most 300 characters), `comment` (a LinkedIn post URL), `reply` (an X post URL). It must be under
|
|
530
|
+
`approvals.required_for`, so the person sees and approves each message; the gateway paces it and holds daily caps per
|
|
531
|
+
account (LinkedIn: 25 DMs, 15 notes, 10 comments; X: 15 DMs, 10 replies). Refusals come back as text codes:
|
|
532
|
+
`NOT_SIGNED_IN` (ask the person to sign in again, under "could not do"), `NOT_CONNECTED`, `DAILY_CAP`,
|
|
533
|
+
`BLOCKED_BY_SITE`. Browser time is charged to the run's budget.
|
|
520
534
|
- Outbound HTTPS is open, so code may read public pages and APIs directly (`httpx`, `curl_cffi`). Anything that acts
|
|
521
535
|
on an account still goes through a tool, so the person's connection and approvals apply.
|
|
522
536
|
- Locally, `gravity connect <app>` attaches **your** account to your local runs (`gravity connect` alone lists what
|
|
@@ -818,6 +832,7 @@ checkpointing, durable approvals, `gravity.ask()`, action buttons, chains and ou
|
|
|
818
832
|
| `APPROVAL_REQUIRED` (inside a hosted run) | A gated tool was called through the plain MCP client | Load tools with `await gravity.tools()` (section 5) |
|
|
819
833
|
| `INVALID_APPROVAL`, `INVALID_TRIGGER`, `INVALID_KIND`, `INVALID_OUTPUT` | `required_for` names an undeclared tool; nothing can start the agent, a bad cron, both day fields, faster than 5 minutes; a monitor without a trigger; a bad output name or destination | Section 4 |
|
|
820
834
|
| `INVALID_CONDITION`, `INVALID_ACTION`, `INVALID_MEMORY` | a `show_if` on a later input or a value it cannot take, a `choose_one` member required alone; an action that requires or chains to an undeclared one or loops, no default action; `memory` beside `state`, a bad collection | Section 4 |
|
|
835
|
+
| `INVALID_ACCOUNT` | an account on a site other than linkedin.com or x.com, a name used twice or not an identifier; `browser.*` declared with no account to act through | Section 6 |
|
|
821
836
|
| `UNDECLARED_TOOL_CALL` | Source names a catalog write tool the manifest does not grant | Declare and gate it, remove the call, or make a deliver-only tool an output `destination` |
|
|
822
837
|
| `FILE_INPUT_UNSUPPORTED` (hosted build only) | A required `file` input | Make it optional, add a text fallback |
|
|
823
838
|
| `SECRET_FOUND` (hosted build only) | The secret scanner matched something in the upload | Remove it, fixtures included; rotate it if it was real |
|
|
@@ -147,6 +147,23 @@ def test_push_sends_exactly_the_built_zip(project: Path, fake_push):
|
|
|
147
147
|
assert sent_sha == hashlib.sha256(sent_bytes).hexdigest()
|
|
148
148
|
|
|
149
149
|
|
|
150
|
+
def test_push_leaves_unset_fields_out_of_the_manifest_header(project: Path, fake_push):
|
|
151
|
+
"""Nulls cost header bytes (12 KB limit) and the platform treats a missing field as unset."""
|
|
152
|
+
build_command(project, skip_validate=True)
|
|
153
|
+
push_command(project)
|
|
154
|
+
manifest = fake_push[0][0]
|
|
155
|
+
|
|
156
|
+
def nulls(node, path=""):
|
|
157
|
+
if isinstance(node, dict):
|
|
158
|
+
return [p for k, v in node.items() for p in ([f"{path}.{k}"] if v is None else nulls(v, f"{path}.{k}"))]
|
|
159
|
+
if isinstance(node, list):
|
|
160
|
+
return [p for i, v in enumerate(node) for p in nulls(v, f"{path}[{i}]")]
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
assert nulls(manifest) == []
|
|
164
|
+
assert manifest["name"] == "demo" and "inputs" in manifest
|
|
165
|
+
|
|
166
|
+
|
|
150
167
|
def test_push_refuses_a_zip_over_the_cap(project: Path, fake_push, monkeypatch):
|
|
151
168
|
"""The control plane drops the socket at 30 MB, which httpx reports as 'Can't reach Gravity'."""
|
|
152
169
|
build_command(project, skip_validate=True)
|
|
@@ -164,7 +164,7 @@ wheels = [
|
|
|
164
164
|
|
|
165
165
|
[[package]]
|
|
166
166
|
name = "gravity-cli"
|
|
167
|
-
version = "0.7.
|
|
167
|
+
version = "0.7.4"
|
|
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.7.
|
|
204
|
+
version = "0.7.4"
|
|
205
205
|
source = { editable = "packages/gravity-schema" }
|
|
206
206
|
dependencies = [
|
|
207
207
|
{ name = "pydantic" },
|
|
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.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/catalog_imported.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/gen_catalog.py
RENAMED
|
File without changes
|
|
File without changes
|
{gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/rules_cases.json
RENAMED
|
File without changes
|
{gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/source_scan.py
RENAMED
|
File without changes
|
{gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/gravity_schema/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
{gravity_cli-0.7.2 → gravity_cli-0.7.4}/packages/gravity-schema/tests/test_actions_memory.py
RENAMED
|
File without changes
|
{gravity_cli-0.7.2 → gravity_cli-0.7.4}/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
|
|
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.7.2 → gravity_cli-0.7.4}/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
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.7.2 → gravity_cli-0.7.4}/src/gravity_cli/templates/structured/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gravity_cli-0.7.2 → gravity_cli-0.7.4}/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
|
|
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
|