gravity-schema 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.
Files changed (35) hide show
  1. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/PKG-INFO +1 -1
  2. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/catalog.yaml +27 -0
  3. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/contract.py +4 -0
  4. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/errors.py +1 -0
  5. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/manifest.py +63 -0
  6. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/pyproject.toml +1 -1
  7. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/schema/manifest.schema.json +46 -0
  8. gravity_schema-0.7.4/tests/test_accounts.py +42 -0
  9. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/.gitignore +0 -0
  10. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/LICENSE +0 -0
  11. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/NOTICE +0 -0
  12. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/README.md +0 -0
  13. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/__init__.py +0 -0
  14. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/__main__.py +0 -0
  15. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/catalog.py +0 -0
  16. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/catalog_imported.yaml +0 -0
  17. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/cron.py +0 -0
  18. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/export.py +0 -0
  19. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/gen_catalog.py +0 -0
  20. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/rules.py +0 -0
  21. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/rules_cases.json +0 -0
  22. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/source_scan.py +0 -0
  23. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/validation.py +0 -0
  24. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/conftest.py +0 -0
  25. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_actions_memory.py +0 -0
  26. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_catalog_parity.py +0 -0
  27. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_cron.py +0 -0
  28. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_errors.py +0 -0
  29. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_json_schema.py +0 -0
  30. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_kind_outputs.py +0 -0
  31. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_manifest.py +0 -0
  32. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_rules.py +0 -0
  33. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_slots.py +0 -0
  34. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_source_scan.py +0 -0
  35. {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gravity-schema
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: Gravity agent manifest contract — models, validation, and JSON Schema export
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -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,6 +1,6 @@
1
1
  [project]
2
2
  name = "gravity-schema"
3
- version = "0.7.2"
3
+ version = "0.7.4"
4
4
  description = "Gravity agent manifest contract — models, validation, and JSON Schema export"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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"
File without changes
File without changes
File without changes