gravity-cli 0.7.5__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/PKG-INFO +2 -2
  2. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/catalog.yaml +54 -0
  3. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/manifest.py +42 -0
  4. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/rules.py +15 -0
  5. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/pyproject.toml +1 -1
  6. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/schema/manifest.schema.json +98 -0
  7. gravity_cli-0.8.0/packages/gravity-schema/tests/test_slots.py +104 -0
  8. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/pyproject.toml +2 -2
  9. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/BUILDER_GUIDE.md +30 -1
  10. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/CHANGELOG.md +10 -0
  11. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/docs/Gravity.agent.md +28 -3
  12. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/uv.lock +2 -2
  13. gravity_cli-0.7.5/packages/gravity-schema/tests/test_slots.py +0 -52
  14. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/.env.example +0 -0
  15. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/.github/workflows/ci.yml +0 -0
  16. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/.github/workflows/release.yml +0 -0
  17. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/.gitignore +0 -0
  18. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/AGENT_STANDARD.md +0 -0
  19. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/LICENSE +0 -0
  20. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/NOTICE +0 -0
  21. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/README.md +0 -0
  22. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/RELEASING.md +0 -0
  23. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/TEMPLATES.md +0 -0
  24. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/LICENSE +0 -0
  25. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/NOTICE +0 -0
  26. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/README.md +0 -0
  27. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/__init__.py +0 -0
  28. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/__main__.py +0 -0
  29. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/catalog.py +0 -0
  30. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/catalog_imported.yaml +0 -0
  31. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/contract.py +0 -0
  32. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/cron.py +0 -0
  33. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/errors.py +0 -0
  34. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/export.py +0 -0
  35. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/gen_catalog.py +0 -0
  36. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/rules_cases.json +0 -0
  37. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/source_scan.py +0 -0
  38. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/gravity_schema/validation.py +0 -0
  39. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/conftest.py +0 -0
  40. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_accounts.py +0 -0
  41. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_actions_memory.py +0 -0
  42. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_catalog_parity.py +0 -0
  43. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_cron.py +0 -0
  44. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_errors.py +0 -0
  45. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_json_schema.py +0 -0
  46. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_kind_outputs.py +0 -0
  47. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_manifest.py +0 -0
  48. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_rules.py +0 -0
  49. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_source_scan.py +0 -0
  50. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/packages/gravity-schema/tests/test_validation.py +0 -0
  51. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/__init__.py +0 -0
  52. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/__main__.py +0 -0
  53. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/_runner_script.py +0 -0
  54. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/api.py +0 -0
  55. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/changelog.py +0 -0
  56. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/cli.py +0 -0
  57. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/__init__.py +0 -0
  58. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/adapt.py +0 -0
  59. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/build.py +0 -0
  60. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/changelog.py +0 -0
  61. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/connect.py +0 -0
  62. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/dev.py +0 -0
  63. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/docs.py +0 -0
  64. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/guide.py +0 -0
  65. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/init.py +0 -0
  66. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/login.py +0 -0
  67. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/memory.py +0 -0
  68. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/push.py +0 -0
  69. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/run.py +0 -0
  70. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/runs.py +0 -0
  71. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/schedule.py +0 -0
  72. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/tools.py +0 -0
  73. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/update.py +0 -0
  74. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/commands/validate.py +0 -0
  75. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/config.py +0 -0
  76. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/dev.py +0 -0
  77. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/dev_page/index.html +0 -0
  78. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/docs/problem-to-agent.md +0 -0
  79. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/inputs.py +0 -0
  80. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/localrun.py +0 -0
  81. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/manifest.py +0 -0
  82. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/project.py +0 -0
  83. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/chat/.gitignore +0 -0
  84. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/chat/manifest.yaml +0 -0
  85. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/chat/requirements.txt +0 -0
  86. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/chat/src/agent.py +0 -0
  87. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/deepagent/.gitignore +0 -0
  88. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/deepagent/manifest.yaml +0 -0
  89. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/deepagent/requirements.txt +0 -0
  90. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/deepagent/src/agent.py +0 -0
  91. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/minimal/.gitignore +0 -0
  92. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/minimal/manifest.yaml +0 -0
  93. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/minimal/requirements.txt +0 -0
  94. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/minimal/src/agent.py +0 -0
  95. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/.gitignore +0 -0
  96. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/manifest.yaml +0 -0
  97. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/requirements.txt +0 -0
  98. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/src/agent.py +0 -0
  99. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/src/gateway.py +0 -0
  100. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/src/nodes/agent.py +0 -0
  101. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/src/prompts.py +0 -0
  102. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/templates/structured/src/state.py +0 -0
  103. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/timeline.py +0 -0
  104. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/ui.py +0 -0
  105. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/updater.py +0 -0
  106. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/src/gravity_cli/workspace.py +0 -0
  107. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/starter/manifest.yaml +0 -0
  108. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/starter/requirements.txt +0 -0
  109. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/starter/src/agent.py +0 -0
  110. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/starter/src/summary.py +0 -0
  111. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/starter/tests/test_summary.py +0 -0
  112. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/conftest.py +0 -0
  113. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_adapt.py +0 -0
  114. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_agent_guide.py +0 -0
  115. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_agents.py +0 -0
  116. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_api.py +0 -0
  117. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_build_push.py +0 -0
  118. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_changelog.py +0 -0
  119. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_connect.py +0 -0
  120. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_dev.py +0 -0
  121. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_gateway_token.py +0 -0
  122. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_inputs_memory.py +0 -0
  123. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_localrun.py +0 -0
  124. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_manifest_integration.py +0 -0
  125. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_run.py +0 -0
  126. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_runs.py +0 -0
  127. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_schedule.py +0 -0
  128. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_session.py +0 -0
  129. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_templates.py +0 -0
  130. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_timeline.py +0 -0
  131. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_tools.py +0 -0
  132. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_ui.py +0 -0
  133. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_updater.py +0 -0
  134. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_validate.py +0 -0
  135. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/tests/test_welcome.py +0 -0
  136. {gravity_cli-0.7.5 → gravity_cli-0.8.0}/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.5
3
+ Version: 0.8.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.7.5
9
+ Requires-Dist: gravity-schema==0.8.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
@@ -327,6 +327,24 @@
327
327
  guarantee: certified
328
328
  side_effect: write
329
329
 
330
+ - name: x.user_lookup
331
+ slug: TWITTER_USER_LOOKUP_BY_USERNAME
332
+ toolkit: twitter
333
+ description: look up an X account by its @username (without the @) through the person's own X developer app; returns its id (what x.send_dm takes), name, bio and counts
334
+ scopes: [users.read]
335
+ breadth: generic
336
+ guarantee: certified
337
+ side_effect: read
338
+
339
+ - name: x.send_dm
340
+ slug: TWITTER_SEND_A_NEW_MESSAGE_TO_A_USER
341
+ toolkit: twitter
342
+ description: send a direct message on X to one account (participant_id from x.user_lookup) from the person's own X developer app, billed to their X API account; X delivers it only when the recipient accepts messages from them
343
+ scopes: [dm.write, dm.read, tweet.read, users.read]
344
+ breadth: generic
345
+ guarantee: certified
346
+ side_effect: write
347
+
330
348
  - name: forms.create
331
349
  slug: GOOGLEFORMS_CREATE_FORM
332
350
  toolkit: googleforms
@@ -399,6 +417,42 @@
399
417
  guarantee: community
400
418
  side_effect: read
401
419
 
420
+ - name: byteforms.create_form
421
+ slug: BYTEFORMS_CREATE_FORM
422
+ toolkit: byteforms
423
+ description: create a public Byteforms form from text, email and phone fields, with an optional close date (stop_submissions_after) and submission cap; returns its id and public_id for the link; runs on Gravity's own Byteforms account where Gravity connected one
424
+ scopes: []
425
+ breadth: generic
426
+ guarantee: community
427
+ side_effect: write
428
+
429
+ - name: byteforms.get_form
430
+ slug: BYTEFORMS_GET_FORM_BY_ID
431
+ toolkit: byteforms
432
+ description: read one Byteforms form by its numeric id (fields, options, link)
433
+ scopes: []
434
+ breadth: generic
435
+ guarantee: community
436
+ side_effect: read
437
+
438
+ - name: byteforms.list_forms
439
+ slug: BYTEFORMS_GET_ALL_FORMS2
440
+ toolkit: byteforms
441
+ description: list the forms on the Byteforms account
442
+ scopes: []
443
+ breadth: generic
444
+ guarantee: community
445
+ side_effect: read
446
+
447
+ - name: byteforms.get_responses
448
+ slug: BYTEFORMS_GET_FORM_RESPONSES
449
+ toolkit: byteforms
450
+ description: read the responses to one Byteforms form by its numeric id, newest first, with a cursor for more
451
+ scopes: []
452
+ breadth: generic
453
+ guarantee: community
454
+ side_effect: read
455
+
402
456
  - name: scrapecreators.linkedin_profile
403
457
  slug: SCRAPECREATORS_GET_LINKEDIN_PROFILE
404
458
  toolkit: scrapecreators
@@ -596,6 +596,15 @@ class OutputSpec(BaseModel):
596
596
  return v
597
597
 
598
598
 
599
+ class AdapterOption(BaseModel):
600
+ """How one pick reads to the person when the app's name alone does not say it."""
601
+
602
+ model_config = _STRICT
603
+
604
+ title: Annotated[str, Field(min_length=1, max_length=C.MAX_LABEL_LENGTH)]
605
+ about: Annotated[str, Field(max_length=C.MAX_LABEL_LENGTH)] | None = None
606
+
607
+
599
608
  class SlotSpec(BaseModel):
600
609
  """A swappable part. Adapter <name> is src/adapters/<slot>/<name>.py; its tools are granted only when picked."""
601
610
 
@@ -606,9 +615,28 @@ class SlotSpec(BaseModel):
606
615
  adapters: dict[str, list[str]] # adapter name -> the catalog tools it calls
607
616
  # The consumer may pick several adapters; the pick is a list and every pick's tools are granted.
608
617
  multiple: bool = False
618
+ # Asked when this action runs instead of at setup, after the action's own questions; the pick is kept for later runs.
619
+ ask_in: Annotated[str, Field(max_length=C.MAX_NAME_LENGTH)] | None = None
620
+ # With ask_in: asked only when this holds (names the action's inputs, setup inputs or other slots).
621
+ show_if: Annotated[ShowIf | None, Field(max_length=C.MAX_INPUTS)] = None
622
+ # Per adapter: the option's title and one line on what picking it means.
623
+ options: dict[str, AdapterOption] = Field(default_factory=dict)
609
624
 
610
625
  @model_validator(mode="after")
611
626
  def _check(self) -> SlotSpec:
627
+ unknown = [n for n in self.options if n not in self.adapters]
628
+ if unknown:
629
+ raise PydanticCustomError(
630
+ ErrorCode.INVALID_INPUT.value,
631
+ "Slot option '{value}' is not one of its adapters.",
632
+ {"value": unknown[0], "remediation": f"Describe adapters only: {', '.join(self.adapters)}."},
633
+ )
634
+ if self.show_if and not self.ask_in:
635
+ raise PydanticCustomError(
636
+ ErrorCode.INVALID_CONDITION.value,
637
+ "A slot's show_if needs ask_in: a setup choice is always asked.",
638
+ {"remediation": "Add ask_in: <action>, or drop show_if."},
639
+ )
612
640
  for name, tools in self.adapters.items():
613
641
  if not C.INPUT_NAME_RE.match(name):
614
642
  raise PydanticCustomError(
@@ -1042,6 +1070,20 @@ class Manifest(BaseModel):
1042
1070
  _check_show_if(self.inputs, self.slots, "inputs")
1043
1071
  for a in self.actions:
1044
1072
  _check_show_if(a.inputs, self.slots, f"actions.{a.name}.inputs", earlier=self.inputs)
1073
+ actions = {a.name: a for a in self.actions}
1074
+ for name, slot in self.slots.items():
1075
+ if slot.ask_in is None:
1076
+ continue
1077
+ if slot.ask_in not in actions:
1078
+ raise PydanticCustomError(
1079
+ ErrorCode.INVALID_ACTION.value,
1080
+ "Slot '{value}' is asked in action '{other}', which is not declared.",
1081
+ {"value": name, "other": slot.ask_in, "remediation": "Name an action under `actions`."},
1082
+ )
1083
+ # Checked as a question asked after the action's own: any of them, the setup inputs and the other slots.
1084
+ asked = InputSpec.model_construct(name=name, type=InputType.STATIC_DROPDOWN, show_if=slot.show_if)
1085
+ others = {k: v for k, v in self.slots.items() if k != name}
1086
+ _check_show_if([asked], others, f"slots.{name}", earlier=[*self.inputs, *actions[slot.ask_in].inputs])
1045
1087
  names = {i.name: i for i in self.inputs}
1046
1088
  grouped: set[str] = set()
1047
1089
  for g in self.choose_one:
@@ -83,6 +83,21 @@ def visible_action_inputs(manifest: Manifest, action: ActionSpec, answers: dict[
83
83
  return _walk(action.inputs, [], action_answers, known)
84
84
 
85
85
 
86
+ def action_slots(manifest: Manifest, action: ActionSpec, answers: dict[str, Any],
87
+ action_answers: dict[str, Any]) -> list[str]:
88
+ """The setup choices this action asks after its own questions (slots with ask_in), in manifest order."""
89
+ known = _slot_picks(manifest, answers)
90
+ for name in visible_inputs(manifest, answers):
91
+ if name in answers:
92
+ known[name] = answers[name]
93
+ for name in visible_action_inputs(manifest, action, answers, action_answers):
94
+ if name in action_answers:
95
+ known[name] = action_answers[name]
96
+ return [name for name, slot in manifest.slots.items()
97
+ if slot.ask_in == action.name
98
+ and all(ref in known and _holds(known[ref], want) for ref, want in (slot.show_if or {}).items())]
99
+
100
+
86
101
  def apply_rules(manifest: Manifest, answers: dict[str, Any]) -> dict[str, Any]:
87
102
  """The answers a run gets: slot picks and visible inputs; a hidden input's value is dropped."""
88
103
  keep = set(visible_inputs(manifest, answers)) | set(manifest.slots)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gravity-schema"
3
- version = "0.7.5"
3
+ version = "0.8.0"
4
4
  description = "Gravity agent manifest contract — models, validation, and JSON Schema export"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -110,6 +110,36 @@
110
110
  "title": "ActionSpec",
111
111
  "type": "object"
112
112
  },
113
+ "AdapterOption": {
114
+ "additionalProperties": false,
115
+ "description": "How one pick reads to the person when the app's name alone does not say it.",
116
+ "properties": {
117
+ "about": {
118
+ "anyOf": [
119
+ {
120
+ "maxLength": 200,
121
+ "type": "string"
122
+ },
123
+ {
124
+ "type": "null"
125
+ }
126
+ ],
127
+ "default": null,
128
+ "title": "About"
129
+ },
130
+ "title": {
131
+ "maxLength": 200,
132
+ "minLength": 1,
133
+ "title": "Title",
134
+ "type": "string"
135
+ }
136
+ },
137
+ "required": [
138
+ "title"
139
+ ],
140
+ "title": "AdapterOption",
141
+ "type": "object"
142
+ },
113
143
  "AgentKind": {
114
144
  "description": "transform: input in, result out (the platform delivers). interactive: tools mid-flow,\nmaybe several turns. monitor: scheduled or event-driven, remembers the last run.",
115
145
  "enum": [
@@ -693,6 +723,19 @@
693
723
  "title": "Adapters",
694
724
  "type": "object"
695
725
  },
726
+ "ask_in": {
727
+ "anyOf": [
728
+ {
729
+ "maxLength": 64,
730
+ "type": "string"
731
+ },
732
+ {
733
+ "type": "null"
734
+ }
735
+ ],
736
+ "default": null,
737
+ "title": "Ask In"
738
+ },
696
739
  "default": {
697
740
  "title": "Default",
698
741
  "type": "string"
@@ -706,6 +749,61 @@
706
749
  "default": false,
707
750
  "title": "Multiple",
708
751
  "type": "boolean"
752
+ },
753
+ "options": {
754
+ "additionalProperties": {
755
+ "$ref": "#/$defs/AdapterOption"
756
+ },
757
+ "title": "Options",
758
+ "type": "object"
759
+ },
760
+ "show_if": {
761
+ "anyOf": [
762
+ {
763
+ "additionalProperties": {
764
+ "anyOf": [
765
+ {
766
+ "type": "string"
767
+ },
768
+ {
769
+ "type": "boolean"
770
+ },
771
+ {
772
+ "type": "integer"
773
+ },
774
+ {
775
+ "type": "number"
776
+ },
777
+ {
778
+ "items": {
779
+ "anyOf": [
780
+ {
781
+ "type": "string"
782
+ },
783
+ {
784
+ "type": "boolean"
785
+ },
786
+ {
787
+ "type": "integer"
788
+ },
789
+ {
790
+ "type": "number"
791
+ }
792
+ ]
793
+ },
794
+ "type": "array"
795
+ }
796
+ ]
797
+ },
798
+ "maxProperties": 50,
799
+ "type": "object"
800
+ },
801
+ {
802
+ "type": "null"
803
+ }
804
+ ],
805
+ "default": null,
806
+ "title": "Show If"
709
807
  }
710
808
  },
711
809
  "required": [
@@ -0,0 +1,104 @@
1
+ """Slots: a swappable adapter per slot; only the picked adapter's tools are granted."""
2
+
3
+ import pytest
4
+ from gravity_schema import dev_catalog, validate_manifest
5
+ from gravity_schema.errors import ManifestValidationError
6
+
7
+ BASE = {
8
+ "name": "digest",
9
+ "runtime": {"entrypoint": "src/agent.py:graph"},
10
+ "permissions": {"tools": ["gmail.read"]},
11
+ "slots": {
12
+ "deliver": {
13
+ "label": "Where to send it",
14
+ "default": "gmail",
15
+ "adapters": {"gmail": ["gmail.draft"], "linear": ["linear.list_teams", "linear.create_issue"]},
16
+ }
17
+ },
18
+ }
19
+
20
+
21
+ def test_only_the_picked_adapter_is_granted():
22
+ m = validate_manifest(BASE, catalog=dev_catalog())
23
+ assert m.tools_for({}) == ["gmail.read", "gmail.draft"]
24
+ assert m.tools_for({"deliver": "linear"}) == ["gmail.read", "linear.list_teams", "linear.create_issue"]
25
+ assert m.default_inputs()["deliver"] == "gmail"
26
+ with pytest.raises(ValueError):
27
+ m.tools_for({"deliver": "../../etc"}) # a pick is never a free path
28
+
29
+
30
+ def test_bad_slots_are_rejected():
31
+ for patch in (
32
+ {"default": "slack"}, # default not an adapter
33
+ {"adapters": {"gmail": ["GMAIL_SEND_EMAIL"]}, "default": "gmail"}, # vendor slug
34
+ ):
35
+ bad = {**BASE, "slots": {"deliver": {**BASE["slots"]["deliver"], **patch}}}
36
+ with pytest.raises(ManifestValidationError):
37
+ validate_manifest(bad)
38
+ clash = {**BASE, "inputs": [{"name": "deliver", "type": "text", "label": "x"}]}
39
+ with pytest.raises(ManifestValidationError):
40
+ validate_manifest(clash)
41
+
42
+
43
+ def test_adapter_tools_are_checked_against_the_catalog():
44
+ bad = {**BASE, "slots": {"deliver": {**BASE["slots"]["deliver"], "adapters": {"gmail": ["gmail.nope"]}}}}
45
+ with pytest.raises(ManifestValidationError) as e:
46
+ validate_manifest(bad, catalog=dev_catalog())
47
+ assert "slots.deliver.adapters.gmail[0]" in str(e.value.issues[0].location)
48
+
49
+
50
+ def test_approvals_may_name_an_adapter_tool():
51
+ m = validate_manifest({**BASE, "approvals": {"required_for": ["gmail.draft"]}})
52
+ assert "gmail.draft" in m.declared_tools
53
+
54
+
55
+ # A choice that only one step needs: asked when that action runs, under the action's own answers.
56
+ ASKED = {
57
+ **BASE,
58
+ "actions": [
59
+ {"name": "find", "label": "Find", "default": True},
60
+ {"name": "reach", "label": "Reach out", "requires": ["find"], "inputs": [
61
+ {"name": "channels", "type": "multi-select", "label": "Where?", "options": ["Email", "X message"], "min": 1}]},
62
+ ],
63
+ "slots": {
64
+ **BASE["slots"],
65
+ "x_send": {
66
+ "label": "How should I send your X messages?",
67
+ "default": "manual",
68
+ "ask_in": "reach",
69
+ "show_if": {"channels": "X message"},
70
+ "adapters": {"x_api": ["gmail.draft"], "manual": []},
71
+ "options": {"x_api": {"title": "My X developer app", "about": "Uses your own keys"},
72
+ "manual": {"title": "I'll send them myself"}},
73
+ },
74
+ },
75
+ }
76
+
77
+
78
+ def test_an_action_asks_its_own_setup_choice():
79
+ from gravity_schema.rules import action_slots, visible_inputs
80
+
81
+ m = validate_manifest(ASKED)
82
+ reach = next(a for a in m.actions if a.name == "reach")
83
+ assert action_slots(m, reach, {}, {"channels": ["Email", "X message"]}) == ["x_send"]
84
+ assert action_slots(m, reach, {}, {"channels": ["Email"]}) == []
85
+ assert action_slots(m, m.actions[0], {}, {}) == []
86
+ assert visible_inputs(m, {}) == [] # never a setup question
87
+ assert m.tools_for({}) == ["gmail.read", "gmail.draft"] # the default pick grants nothing more
88
+ assert m.tools_for({"x_send": "x_api"}) == ["gmail.read", "gmail.draft"]
89
+ assert m.slots["x_send"].options["manual"].about is None
90
+
91
+
92
+ def test_bad_action_asked_slots_are_rejected():
93
+ slot = ASKED["slots"]["x_send"]
94
+ for patch in (
95
+ {"ask_in": "nowhere"}, # not an action
96
+ {"show_if": {"channels": "Fax"}}, # a value channels cannot take
97
+ {"show_if": {"later": "x"}}, # names nothing asked before
98
+ {"ask_in": None}, # show_if without ask_in
99
+ {"options": {"carrier_pigeon": {"title": "Pigeon"}}}, # describes no adapter
100
+ {"options": {"manual": {"title": ""}}}, # an empty title
101
+ ):
102
+ bad = {**ASKED, "slots": {**ASKED["slots"], "x_send": {**slot, **patch}}}
103
+ with pytest.raises(ManifestValidationError):
104
+ validate_manifest(bad)
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "gravity-cli"
3
- version = "0.7.5"
3
+ version = "0.8.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.7.5", # released in lockstep: always the same version as this package
10
+ "gravity-schema==0.8.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",
@@ -90,6 +90,9 @@ inputs: # becomes the user's form; arrives as state[
90
90
  type: email # text | textarea | number | boolean | static-dropdown | email | url | date | secret | file
91
91
  label: "Where to send the digest"
92
92
  required: true
93
+ # number, email, url, date and boolean answers are checked on every hosted run: a wrong
94
+ # form is refused on the question ("Enter one email address"); a url typed as
95
+ # example.com arrives as https://example.com
93
96
 
94
97
  permissions:
95
98
  tools: # EVERY tool your code calls, by Gravity name (§7)
@@ -457,7 +460,10 @@ that only works for tools loaded with `await gravity.tools()`: a gated call made
457
460
  fails there with `APPROVAL_REQUIRED`. The example's `load_tools()` does this, and falls back to the MCP client
458
461
  when `gravity` is not installed (`gravity run`, `gravity dev`, tests). After the decision the node that made
459
462
  the call runs again from its start, so keep the gated call last in its node. If you wrap the call in a broad
460
- `except`, re-raise `langgraph.errors.GraphBubbleUp` first (`except GraphBubbleUp: raise`), or the pause is swallowed.
463
+ `except`, re-raise `langgraph.errors.GraphBubbleUp` first (`except GraphBubbleUp: raise`). If you forget, the
464
+ hosted platform raises the caught pause again when the node ends, so the person is still asked; but the code after
465
+ your `except` has already run once (it thought the call failed) and runs again after the decision. Under
466
+ `gravity run` nothing raises it again, so test the approval path locally.
461
467
 
462
468
  ### 6.2 Platform adapters: let the user choose where results go
463
469
 
@@ -489,6 +495,27 @@ slots:
489
495
  linear: [linear.list_teams, linear.create_issue, linear.get_issue]
490
496
  ```
491
497
 
498
+ A choice that only one step needs is asked by that step instead of at setup: give the slot
499
+ `ask_in: <action>` (and a `show_if` on that action's answers if it applies only sometimes). The
500
+ step asks it after its own questions, the run gets the pick's tools, and the pick is kept for later
501
+ runs. Until then the `default` applies, so default to the pick that needs no account. `options`
502
+ says what each pick means:
503
+
504
+ ```yaml
505
+ slots:
506
+ x_send:
507
+ label: "How should I send your X messages?"
508
+ default: manual
509
+ ask_in: reach_out
510
+ show_if: { channels: "X message" }
511
+ adapters:
512
+ x_api: [x.user_lookup, x.send_dm]
513
+ manual: []
514
+ options:
515
+ x_api: { title: "My X developer app", about: "Sends through your own X app; X bills your developer account." }
516
+ manual: { title: "I'll send them myself", about: "I write each message; you send it." }
517
+ ```
518
+
492
519
  `src/adapters/deliver/__init__.py`: the contract every adapter follows:
493
520
 
494
521
  ```python
@@ -657,6 +684,8 @@ memory.previous() # the last finished run of this conversation: action, inputs
657
684
  - There is no user id in the API: the run decides whose memory it is, so your code can never reach
658
685
  another user's. A write over a limit raises `MemoryQuotaError` and writes nothing.
659
686
  - Items expire `keep_days` after their last write. The user sees and deletes what the agent keeps.
687
+ - Each call is one blocking request. Read a collection once (`find(limit=500)`) and reuse the list instead of
688
+ calling `get` per record; check many keys with one `has_many`.
660
689
  - Under `gravity run` the same calls use `.gravity/memory/`; `gravity memory ls` shows it.
661
690
 
662
691
  ### 6.5 Actions: more than one thing to do
@@ -4,6 +4,16 @@ 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.8.0 - 2026-10-25
8
+
9
+ - new: A step can ask its own setup choice. A slot with `ask_in: <action>` is asked when that action runs (after its own questions, under its own `show_if`), not at setup; the run gets the pick's tools and asks for its app if needed. `options` gives each pick a title and a line on what it means.
10
+ - new: X through the person's own X developer app. x.user_lookup finds an account by @username and x.send_dm sends it a message, billed to their X API account (Composio's X toolkit has no shared app, so each person connects their own).
11
+ - improved: Docs on what the platform does with a caught approval pause, and on keeping memory calls few.
12
+
13
+ ## 0.7.6 - 2026-10-24
14
+
15
+ - new: Byteforms tools. byteforms.create_form builds a public form from text, email and phone fields with a close date, and byteforms.get_responses reads what people sent (plus get_form and list_forms). Where Gravity has connected its own Byteforms account, every user's runs use it and nobody connects anything.
16
+
7
17
  ## 0.7.5 - 2026-10-24
8
18
 
9
19
  - new: ScrapeCreators tools on Gravity's account. scrapecreators.linkedin_profile reads a public LinkedIn profile in full (experience with descriptions, education, recent activity) where LinkedIn refuses a plain read, plus linkedin_post, twitter_profile, twitter_tweets, reddit_search (no Reddit API key) and social_profiles. They run through Composio Instant, charged to the run's budget.
@@ -291,6 +291,17 @@ slots: # swappable adapters (section 10)
291
291
  adapters:
292
292
  gravity: [] # kept in Gravity
293
293
  gmail: [gmail.draft]
294
+ x_send: # a choice only one step needs: asked when that step runs, not at setup
295
+ label: "How should I send your X messages?"
296
+ default: manual
297
+ ask_in: reply_drafts # the action that asks it, after its own questions; the pick is kept for later runs
298
+ # show_if: { channels: "X message" } # only when that step's answers (or setup answers, or other slots) match
299
+ adapters:
300
+ x_api: [x.user_lookup, x.send_dm]
301
+ manual: []
302
+ options: # how each pick reads, when the app's name alone does not say it
303
+ x_api: { title: "My X developer app", about: "Sends through your own X app; X bills your developer account." }
304
+ manual: { title: "I'll send them myself", about: "I write each message; you send it." }
294
305
 
295
306
  actions: # what the agent can do (section 10); leave out for one entry point
296
307
  - name: digest
@@ -342,7 +353,11 @@ Rules the validator enforces beyond field types:
342
353
  scheduled run from a manual one: no trigger field is sent.
343
354
  - **Triggers**: something must be able to start the agent; `kind: monitor` needs a schedule or a webhook.
344
355
  - **Slots**: the pick arrives as `state["inputs"][<slot>]` (a list for `multiple: true`). A run is granted
345
- `permissions.tools` plus only the picked adapters' tools.
356
+ `permissions.tools` plus only the picked adapters' tools. A slot with `ask_in: <action>` is not asked at setup: that
357
+ action asks it after its own questions (when its `show_if` holds; it may name that action's inputs), the run gets the
358
+ pick's tools and asks to connect the pick's app if needed, and the pick is kept for later runs. Until it is asked, the
359
+ `default` applies, so make the default the pick that needs nothing (no account, no tools). `options` gives each
360
+ adapter a `title` and an `about` line for the person; without it the app's name is shown.
346
361
  - **Conditions** (`INVALID_CONDITION`): a `show_if` names a slot or an input declared above it, with values that
347
362
  input can take (an option, an adapter, `true`/`false` for a boolean). Every condition in one `show_if` must hold;
348
363
  a multi-select or multiple slot holds when any pick matches. A hidden question is never asked, never required,
@@ -459,7 +474,8 @@ async def graph():
459
474
  | No checkpointer or store passed to `compile()` | `wf.compile()` | passing one: it is replaced |
460
475
  | `messages` only for conversations, with `add_messages` | section 3 | a plain list reducer (replaces the history) |
461
476
  | No `interrupt()` of your own | gated tools pause the run for you; `gravity.ask()` asks a question | a bare `interrupt()`: `UNSUPPORTED_INTERRUPT` fails the run |
462
- | A gated call is never wrapped in a broad `except` | call it plainly; if you must catch, `except GraphBubbleUp: raise` first (`from langgraph.errors import GraphBubbleUp`) | `except Exception` around it: that swallows the pause for approval, so the message is sent unasked or never sent |
477
+ | A gated call is never wrapped in a broad `except` | call it plainly; if you must catch, `except GraphBubbleUp: raise` first (`from langgraph.errors import GraphBubbleUp`) | `except Exception` around it: that catches the pause for approval. Hosted, the platform raises a caught pause again when the node ends, so the person is still asked, but everything after your `except` already ran once and runs again on resume; under `gravity run` nothing catches it for you |
478
+ | Memory calls are few per run | `find(limit=500)` once and reuse the list; `has_many(keys)` for many keys | `get(key)` inside a loop over every record: each call is one blocking request, and hundreds of them stall the run |
463
479
 
464
480
  Also:
465
481
 
@@ -578,13 +594,18 @@ one at a time.
578
594
  | `static-dropdown` | One of `options` | string |
579
595
  | `multi-select` | Any of `options`, between `min` and `max` | list of strings |
580
596
  | `action-choice` | One of the agent's `actions` (that action runs) | string, and `state["actions"]` is that action |
581
- | `email`, `url`, `date` | A validated field | string |
597
+ | `email`, `url`, `date` | A validated field: a wrong form is refused on the question itself ("Enter a web address, like example.com") | string; a `url` typed as `example.com` arrives as `https://example.com` |
582
598
  | `secret` | A masked field, sealed, never shown again | string, for that run only |
583
599
  | `file` | An upload limited by `accept` | a signed https link: accept a link, a path or text in your loader |
584
600
 
585
601
  - Hosted, **every run is checked server-side**: a visible `required` input must be there for manual, chat, webhook
586
602
  and scheduled runs alike, and a hidden one (its `show_if` fails, or the unfilled half of a `choose_one`) is
587
603
  dropped. Write `show_if: { deliver: gmail }` instead of "only when delivering to Gmail" in a label.
604
+ - The **type is checked on every run** too: a `number`, `email`, `url`, `date` or `boolean` answer of the wrong
605
+ form ("fifty", "not-an-email", "my notion page") is refused with what to type, so your code never sees it. Pick
606
+ the type that matches: a website or a page link is `url`, not `text`. A number cannot carry `min`/`max` (only a
607
+ `multi-select` can), so when your agent keeps a number in a range, say so in the label and in the result ("You
608
+ asked for 50 leads; one run looks for 3 to 20") instead of changing it silently.
588
609
  - A **required** `file` input on its own is refused by the hosted build (`FILE_INPUT_UNSUPPORTED`) although
589
610
  `gravity validate` accepts it. For "upload or paste", put the file and a textarea in a `choose_one` group with
590
611
  `required: true`: either one satisfies it. Scheduled runs cannot carry a file input.
@@ -712,6 +733,10 @@ memory.previous() # this conversation's last fin
712
733
  and deletes what an agent keeps on My agents; the builder sees counts per collection, never content.
713
734
  - `previous()` is `{"action", "actions", "inputs" (no secrets), "result", "finished_at", "truncated"}`; a result
714
735
  over 256 KB is left out and `summary` describes its shape instead.
736
+ - Every call is one blocking request to the platform (about a third of a second), even inside an `async` node.
737
+ Read a collection once with `find(limit=...)` and keep the list, check many keys with `has_many`, and write only
738
+ what changed. A run that makes hundreds of calls in a row is slow; the platform keeps it alive, but the person
739
+ waits.
715
740
  - Update memory **after** the side effect succeeded, so a failed delivery is retried. Keep the notebook small.
716
741
  A follow-up such as "compare 1 and 2" can be answered from memory with no search.
717
742
  - Under `gravity run` the same calls use `.gravity/memory/` with the same limits, if the `gravity` SDK is installed
@@ -164,7 +164,7 @@ wheels = [
164
164
 
165
165
  [[package]]
166
166
  name = "gravity-cli"
167
- version = "0.7.5"
167
+ version = "0.8.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.7.5"
204
+ version = "0.8.0"
205
205
  source = { editable = "packages/gravity-schema" }
206
206
  dependencies = [
207
207
  { name = "pydantic" },
@@ -1,52 +0,0 @@
1
- """Slots: a swappable adapter per slot; only the picked adapter's tools are granted."""
2
-
3
- import pytest
4
- from gravity_schema import dev_catalog, validate_manifest
5
- from gravity_schema.errors import ManifestValidationError
6
-
7
- BASE = {
8
- "name": "digest",
9
- "runtime": {"entrypoint": "src/agent.py:graph"},
10
- "permissions": {"tools": ["gmail.read"]},
11
- "slots": {
12
- "deliver": {
13
- "label": "Where to send it",
14
- "default": "gmail",
15
- "adapters": {"gmail": ["gmail.draft"], "linear": ["linear.list_teams", "linear.create_issue"]},
16
- }
17
- },
18
- }
19
-
20
-
21
- def test_only_the_picked_adapter_is_granted():
22
- m = validate_manifest(BASE, catalog=dev_catalog())
23
- assert m.tools_for({}) == ["gmail.read", "gmail.draft"]
24
- assert m.tools_for({"deliver": "linear"}) == ["gmail.read", "linear.list_teams", "linear.create_issue"]
25
- assert m.default_inputs()["deliver"] == "gmail"
26
- with pytest.raises(ValueError):
27
- m.tools_for({"deliver": "../../etc"}) # a pick is never a free path
28
-
29
-
30
- def test_bad_slots_are_rejected():
31
- for patch in (
32
- {"default": "slack"}, # default not an adapter
33
- {"adapters": {"gmail": ["GMAIL_SEND_EMAIL"]}, "default": "gmail"}, # vendor slug
34
- ):
35
- bad = {**BASE, "slots": {"deliver": {**BASE["slots"]["deliver"], **patch}}}
36
- with pytest.raises(ManifestValidationError):
37
- validate_manifest(bad)
38
- clash = {**BASE, "inputs": [{"name": "deliver", "type": "text", "label": "x"}]}
39
- with pytest.raises(ManifestValidationError):
40
- validate_manifest(clash)
41
-
42
-
43
- def test_adapter_tools_are_checked_against_the_catalog():
44
- bad = {**BASE, "slots": {"deliver": {**BASE["slots"]["deliver"], "adapters": {"gmail": ["gmail.nope"]}}}}
45
- with pytest.raises(ManifestValidationError) as e:
46
- validate_manifest(bad, catalog=dev_catalog())
47
- assert "slots.deliver.adapters.gmail[0]" in str(e.value.issues[0].location)
48
-
49
-
50
- def test_approvals_may_name_an_adapter_tool():
51
- m = validate_manifest({**BASE, "approvals": {"required_for": ["gmail.draft"]}})
52
- assert "gmail.draft" in m.declared_tools
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes