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.
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/PKG-INFO +1 -1
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/catalog.yaml +27 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/contract.py +4 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/errors.py +1 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/manifest.py +63 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/pyproject.toml +1 -1
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/schema/manifest.schema.json +46 -0
- gravity_schema-0.7.4/tests/test_accounts.py +42 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/.gitignore +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/LICENSE +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/NOTICE +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/README.md +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/__init__.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/__main__.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/catalog.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/catalog_imported.yaml +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/cron.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/export.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/gen_catalog.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/rules.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/rules_cases.json +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/source_scan.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/gravity_schema/validation.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/conftest.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_actions_memory.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_catalog_parity.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_cron.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_errors.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_json_schema.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_kind_outputs.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_manifest.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_rules.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_slots.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_source_scan.py +0 -0
- {gravity_schema-0.7.2 → gravity_schema-0.7.4}/tests/test_validation.py +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"
|
|
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
|