vidbyte-cli 0.1.0__py3-none-any.whl

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 (269) hide show
  1. vidbyte_cli/__init__.py +11 -0
  2. vidbyte_cli/__main__.py +6 -0
  3. vidbyte_cli/cli.py +18 -0
  4. vidbyte_cli/commands/__init__.py +86 -0
  5. vidbyte_cli/commands/agent_options.py +53 -0
  6. vidbyte_cli/commands/agents/__init__.py +31 -0
  7. vidbyte_cli/commands/agents/suggestion/__init__.py +38 -0
  8. vidbyte_cli/commands/agents/suggestion/prompts/__init__.py +3 -0
  9. vidbyte_cli/commands/agents/suggestion/prompts/all_categories.md +5 -0
  10. vidbyte_cli/commands/agents/suggestion/prompts/approaches.md +100 -0
  11. vidbyte_cli/commands/agents/suggestion/prompts/avoid.md +100 -0
  12. vidbyte_cli/commands/agents/suggestion/prompts/blockers.md +100 -0
  13. vidbyte_cli/commands/agents/suggestion/prompts/capability.md +100 -0
  14. vidbyte_cli/commands/agents/suggestion/prompts/categories.md +5 -0
  15. vidbyte_cli/commands/agents/suggestion/prompts/category.md +5 -0
  16. vidbyte_cli/commands/agents/suggestion/prompts/completed.md +100 -0
  17. vidbyte_cli/commands/agents/suggestion/prompts/constraint.md +100 -0
  18. vidbyte_cli/commands/agents/suggestion/prompts/context.md +100 -0
  19. vidbyte_cli/commands/agents/suggestion/prompts/count.md +5 -0
  20. vidbyte_cli/commands/agents/suggestion/prompts/critic_model.md +100 -0
  21. vidbyte_cli/commands/agents/suggestion/prompts/decision.md +100 -0
  22. vidbyte_cli/commands/agents/suggestion/prompts/dry_run.md +5 -0
  23. vidbyte_cli/commands/agents/suggestion/prompts/extra_compute.md +5 -0
  24. vidbyte_cli/commands/agents/suggestion/prompts/feedback_accept.md +7 -0
  25. vidbyte_cli/commands/agents/suggestion/prompts/feedback_group.md +7 -0
  26. vidbyte_cli/commands/agents/suggestion/prompts/feedback_project.md +100 -0
  27. vidbyte_cli/commands/agents/suggestion/prompts/feedback_reason.md +98 -0
  28. vidbyte_cli/commands/agents/suggestion/prompts/feedback_reject.md +7 -0
  29. vidbyte_cli/commands/agents/suggestion/prompts/feedback_suggestion.md +101 -0
  30. vidbyte_cli/commands/agents/suggestion/prompts/files.md +100 -0
  31. vidbyte_cli/commands/agents/suggestion/prompts/forbidden.md +100 -0
  32. vidbyte_cli/commands/agents/suggestion/prompts/goal.md +100 -0
  33. vidbyte_cli/commands/agents/suggestion/prompts/handoff.md +7 -0
  34. vidbyte_cli/commands/agents/suggestion/prompts/horizon.md +5 -0
  35. vidbyte_cli/commands/agents/suggestion/prompts/hypotheses.md +100 -0
  36. vidbyte_cli/commands/agents/suggestion/prompts/idea.md +100 -0
  37. vidbyte_cli/commands/agents/suggestion/prompts/in_progress.md +100 -0
  38. vidbyte_cli/commands/agents/suggestion/prompts/input.md +100 -0
  39. vidbyte_cli/commands/agents/suggestion/prompts/library.py +29 -0
  40. vidbyte_cli/commands/agents/suggestion/prompts/max_messages.md +5 -0
  41. vidbyte_cli/commands/agents/suggestion/prompts/max_output_tokens.md +5 -0
  42. vidbyte_cli/commands/agents/suggestion/prompts/max_total_tokens.md +5 -0
  43. vidbyte_cli/commands/agents/suggestion/prompts/mistakes.md +100 -0
  44. vidbyte_cli/commands/agents/suggestion/prompts/outcomes.md +100 -0
  45. vidbyte_cli/commands/agents/suggestion/prompts/project.md +100 -0
  46. vidbyte_cli/commands/agents/suggestion/prompts/project_create.md +7 -0
  47. vidbyte_cli/commands/agents/suggestion/prompts/project_description.md +98 -0
  48. vidbyte_cli/commands/agents/suggestion/prompts/project_group.md +7 -0
  49. vidbyte_cli/commands/agents/suggestion/prompts/project_key.md +96 -0
  50. vidbyte_cli/commands/agents/suggestion/prompts/project_list.md +7 -0
  51. vidbyte_cli/commands/agents/suggestion/prompts/project_title.md +99 -0
  52. vidbyte_cli/commands/agents/suggestion/prompts/provider.md +5 -0
  53. vidbyte_cli/commands/agents/suggestion/prompts/question.md +100 -0
  54. vidbyte_cli/commands/agents/suggestion/prompts/risks.md +100 -0
  55. vidbyte_cli/commands/agents/suggestion/prompts/rounds.md +5 -0
  56. vidbyte_cli/commands/agents/suggestion/prompts/run.md +5 -0
  57. vidbyte_cli/commands/agents/suggestion/prompts/success.md +100 -0
  58. vidbyte_cli/commands/agents/suggestion/prompts/timeout.md +5 -0
  59. vidbyte_cli/commands/agents/suggestion/prompts/trajectory.md +100 -0
  60. vidbyte_cli/commands/agents/suggestion/prompts/view.md +5 -0
  61. vidbyte_cli/commands/agents/suggestion/prompts/view_all.md +5 -0
  62. vidbyte_cli/commands/agents/suggestion/render.py +77 -0
  63. vidbyte_cli/commands/agents/suggestion/request_builder.py +240 -0
  64. vidbyte_cli/commands/agents/suggestion/suggest.py +154 -0
  65. vidbyte_cli/commands/agents/suggestion/suggestion_categories.py +77 -0
  66. vidbyte_cli/commands/agents/suggestion/suggestion_feedback.py +85 -0
  67. vidbyte_cli/commands/agents/suggestion/suggestion_handoff.py +67 -0
  68. vidbyte_cli/commands/agents/suggestion/suggestion_projects.py +95 -0
  69. vidbyte_cli/commands/auth/__init__.py +0 -0
  70. vidbyte_cli/commands/auth/login.py +100 -0
  71. vidbyte_cli/commands/auth/logout.py +41 -0
  72. vidbyte_cli/commands/auth/whoami.py +51 -0
  73. vidbyte_cli/commands/billing/__init__.py +20 -0
  74. vidbyte_cli/commands/billing/prompts/__init__.py +1 -0
  75. vidbyte_cli/commands/billing/prompts/confirm.md +16 -0
  76. vidbyte_cli/commands/billing/prompts/group.md +13 -0
  77. vidbyte_cli/commands/billing/prompts/idempotency_key.md +16 -0
  78. vidbyte_cli/commands/billing/prompts/library.py +18 -0
  79. vidbyte_cli/commands/billing/prompts/top_up.md +16 -0
  80. vidbyte_cli/commands/billing/top_up.py +65 -0
  81. vidbyte_cli/commands/config/__init__.py +0 -0
  82. vidbyte_cli/commands/config/get.py +45 -0
  83. vidbyte_cli/commands/config/set.py +39 -0
  84. vidbyte_cli/commands/provider/__init__.py +9 -0
  85. vidbyte_cli/commands/provider/login.py +102 -0
  86. vidbyte_cli/commands/provider/logout.py +45 -0
  87. vidbyte_cli/commands/provider/whoami.py +49 -0
  88. vidbyte_cli/commands/research/__init__.py +5 -0
  89. vidbyte_cli/commands/research/add.py +41 -0
  90. vidbyte_cli/commands/research/options.py +134 -0
  91. vidbyte_cli/commands/research/render.py +157 -0
  92. vidbyte_cli/commands/research/resume.py +37 -0
  93. vidbyte_cli/commands/research/start.py +37 -0
  94. vidbyte_cli/commands/research/status.py +32 -0
  95. vidbyte_cli/commands/research/thread.py +30 -0
  96. vidbyte_cli/commands/research/threads.py +35 -0
  97. vidbyte_cli/commands/research/watch.py +93 -0
  98. vidbyte_cli/commands/runtime/__init__.py +23 -0
  99. vidbyte_cli/commands/runtime/adversarial_team.py +41 -0
  100. vidbyte_cli/commands/runtime/doctor.py +44 -0
  101. vidbyte_cli/commands/runtime/list.py +47 -0
  102. vidbyte_cli/commands/runtime/persistence.py +141 -0
  103. vidbyte_cli/commands/runtime/same_host_ensemble.py +221 -0
  104. vidbyte_cli/commands/runtime/stages.py +553 -0
  105. vidbyte_cli/commands/runtime/task_board.py +1342 -0
  106. vidbyte_cli/commands/setup/__init__.py +0 -0
  107. vidbyte_cli/commands/setup/doctor.py +64 -0
  108. vidbyte_cli/commands/setup/prompts/__init__.py +1 -0
  109. vidbyte_cli/commands/setup/prompts/browser.md +7 -0
  110. vidbyte_cli/commands/setup/prompts/library.py +18 -0
  111. vidbyte_cli/commands/setup/prompts/star.md +13 -0
  112. vidbyte_cli/commands/setup/star.py +132 -0
  113. vidbyte_cli/lib/__init__.py +0 -0
  114. vidbyte_cli/lib/api/__init__.py +0 -0
  115. vidbyte_cli/lib/api/client.py +236 -0
  116. vidbyte_cli/lib/api/endpoints/__init__.py +0 -0
  117. vidbyte_cli/lib/api/endpoints/auth.py +24 -0
  118. vidbyte_cli/lib/api/endpoints/billing.py +36 -0
  119. vidbyte_cli/lib/api/endpoints/research.py +103 -0
  120. vidbyte_cli/lib/api/endpoints/runtime.py +113 -0
  121. vidbyte_cli/lib/api/problem.py +86 -0
  122. vidbyte_cli/lib/api/response.py +101 -0
  123. vidbyte_cli/lib/api/retry.py +116 -0
  124. vidbyte_cli/lib/api/runtime_payment.py +89 -0
  125. vidbyte_cli/lib/auth/__init__.py +16 -0
  126. vidbyte_cli/lib/auth/credentials.py +223 -0
  127. vidbyte_cli/lib/auth/input.py +59 -0
  128. vidbyte_cli/lib/auth/keyring_store.py +126 -0
  129. vidbyte_cli/lib/auth/provider_credentials.py +157 -0
  130. vidbyte_cli/lib/auth/provider_input.py +54 -0
  131. vidbyte_cli/lib/auth/provider_resolver.py +89 -0
  132. vidbyte_cli/lib/auth/provider_store.py +149 -0
  133. vidbyte_cli/lib/auth/provider_verifier.py +219 -0
  134. vidbyte_cli/lib/auth/resolver.py +63 -0
  135. vidbyte_cli/lib/auth/verifier.py +38 -0
  136. vidbyte_cli/lib/config/__init__.py +25 -0
  137. vidbyte_cli/lib/config/atomic.py +47 -0
  138. vidbyte_cli/lib/config/config.py +141 -0
  139. vidbyte_cli/lib/config/migration.py +81 -0
  140. vidbyte_cli/lib/config/models.py +145 -0
  141. vidbyte_cli/lib/config/paths.py +66 -0
  142. vidbyte_cli/lib/config/resolver.py +137 -0
  143. vidbyte_cli/lib/constants/__init__.py +1 -0
  144. vidbyte_cli/lib/constants/runtime.py +186 -0
  145. vidbyte_cli/lib/errors/__init__.py +12 -0
  146. vidbyte_cli/lib/errors/cli_error.py +86 -0
  147. vidbyte_cli/lib/errors/codes.py +43 -0
  148. vidbyte_cli/lib/errors/failures.py +2910 -0
  149. vidbyte_cli/lib/errors/handler.py +72 -0
  150. vidbyte_cli/lib/files/__init__.py +6 -0
  151. vidbyte_cli/lib/files/documents.py +94 -0
  152. vidbyte_cli/lib/files/store.py +156 -0
  153. vidbyte_cli/lib/io/__init__.py +16 -0
  154. vidbyte_cli/lib/io/attachments.py +129 -0
  155. vidbyte_cli/lib/io/codex_attachments.py +47 -0
  156. vidbyte_cli/lib/io/prompt.py +71 -0
  157. vidbyte_cli/lib/io/streams.py +37 -0
  158. vidbyte_cli/lib/io/terminal.py +66 -0
  159. vidbyte_cli/lib/output/__init__.py +7 -0
  160. vidbyte_cli/lib/output/formats.py +29 -0
  161. vidbyte_cli/lib/output/manager.py +95 -0
  162. vidbyte_cli/lib/output/models.py +49 -0
  163. vidbyte_cli/lib/runtime/__init__.py +9 -0
  164. vidbyte_cli/lib/runtime/application.py +134 -0
  165. vidbyte_cli/lib/runtime/context.py +250 -0
  166. vidbyte_cli/lib/runtime/options.py +159 -0
  167. vidbyte_cli/lib/runtime/version.py +30 -0
  168. vidbyte_cli/lib/runtime_primitives/__init__.py +28 -0
  169. vidbyte_cli/lib/runtime_primitives/executor.py +54 -0
  170. vidbyte_cli/lib/runtime_primitives/gate.py +105 -0
  171. vidbyte_cli/lib/runtime_primitives/hosts.py +45 -0
  172. vidbyte_cli/lib/runtime_primitives/planner.py +79 -0
  173. vidbyte_cli/lib/runtime_primitives/stages.py +338 -0
  174. vidbyte_cli/lib/runtime_primitives/task_board.py +982 -0
  175. vidbyte_cli/lib/runtime_primitives/task_board_checkpoints.py +473 -0
  176. vidbyte_cli/lib/runtime_primitives/task_board_system.md +33 -0
  177. vidbyte_cli/lib/runtime_primitives/task_board_tasks.py +111 -0
  178. vidbyte_cli/lib/runtime_primitives/verification.py +68 -0
  179. vidbyte_cli/services/__init__.py +5 -0
  180. vidbyte_cli/services/ensemble/__init__.py +7 -0
  181. vidbyte_cli/services/ensemble/prompts/__init__.py +6 -0
  182. vidbyte_cli/services/ensemble/prompts/implementer_system.md +546 -0
  183. vidbyte_cli/services/ensemble/prompts/implementer_turn.md +49 -0
  184. vidbyte_cli/services/ensemble/prompts/library.py +188 -0
  185. vidbyte_cli/services/ensemble/prompts/planner_system.md +728 -0
  186. vidbyte_cli/services/ensemble/prompts/planner_turn.md +6 -0
  187. vidbyte_cli/services/ensemble/prompts/role_system.md +104 -0
  188. vidbyte_cli/services/ensemble/prompts/role_turn.md +19 -0
  189. vidbyte_cli/services/ensemble/prompts/selector_final_turn.md +19 -0
  190. vidbyte_cli/services/ensemble/prompts/selector_round_turn.md +17 -0
  191. vidbyte_cli/services/ensemble/prompts/selector_system.md +89 -0
  192. vidbyte_cli/services/ensemble/runner.py +30 -0
  193. vidbyte_cli/services/ensemble/sdk.py +134 -0
  194. vidbyte_cli/services/ensemble/service.py +233 -0
  195. vidbyte_cli/services/ensemble/settings.py +60 -0
  196. vidbyte_cli/services/persistence/__init__.py +6 -0
  197. vidbyte_cli/services/persistence/prompts/__init__.py +6 -0
  198. vidbyte_cli/services/persistence/prompts/library.py +46 -0
  199. vidbyte_cli/services/persistence/prompts/persistence_system.md +1 -0
  200. vidbyte_cli/services/persistence/prompts/persistence_turn.md +3 -0
  201. vidbyte_cli/services/persistence/runner.py +35 -0
  202. vidbyte_cli/services/persistence/session.py +116 -0
  203. vidbyte_cli/services/suggestions/__init__.py +7 -0
  204. vidbyte_cli/services/suggestions/categories.py +270 -0
  205. vidbyte_cli/services/suggestions/context.py +222 -0
  206. vidbyte_cli/services/suggestions/extra_compute.py +53 -0
  207. vidbyte_cli/services/suggestions/handoff.py +173 -0
  208. vidbyte_cli/services/suggestions/message_tools.py +47 -0
  209. vidbyte_cli/services/suggestions/project.py +253 -0
  210. vidbyte_cli/services/suggestions/prompts/__init__.py +1 -0
  211. vidbyte_cli/services/suggestions/prompts/categories/__init__.py +3 -0
  212. vidbyte_cli/services/suggestions/prompts/categories/adjacent_opportunity.md +37 -0
  213. vidbyte_cli/services/suggestions/prompts/categories/alternative.md +38 -0
  214. vidbyte_cli/services/suggestions/prompts/categories/big_bets.md +37 -0
  215. vidbyte_cli/services/suggestions/prompts/categories/bottleneck.md +38 -0
  216. vidbyte_cli/services/suggestions/prompts/categories/business_growth.md +38 -0
  217. vidbyte_cli/services/suggestions/prompts/categories/completion.md +37 -0
  218. vidbyte_cli/services/suggestions/prompts/categories/continuation.md +37 -0
  219. vidbyte_cli/services/suggestions/prompts/categories/coordination.md +37 -0
  220. vidbyte_cli/services/suggestions/prompts/categories/creative_exploration.md +37 -0
  221. vidbyte_cli/services/suggestions/prompts/categories/cross_domain.md +37 -0
  222. vidbyte_cli/services/suggestions/prompts/categories/delegation.md +37 -0
  223. vidbyte_cli/services/suggestions/prompts/categories/experiment.md +37 -0
  224. vidbyte_cli/services/suggestions/prompts/categories/feedback.md +37 -0
  225. vidbyte_cli/services/suggestions/prompts/categories/goal_clarification.md +37 -0
  226. vidbyte_cli/services/suggestions/prompts/categories/immediate_next_steps.md +37 -0
  227. vidbyte_cli/services/suggestions/prompts/categories/investigation.md +37 -0
  228. vidbyte_cli/services/suggestions/prompts/categories/learning.md +37 -0
  229. vidbyte_cli/services/suggestions/prompts/categories/leverage.md +37 -0
  230. vidbyte_cli/services/suggestions/prompts/categories/long_term_directions.md +37 -0
  231. vidbyte_cli/services/suggestions/prompts/categories/preparation.md +37 -0
  232. vidbyte_cli/services/suggestions/prompts/categories/prerequisite.md +37 -0
  233. vidbyte_cli/services/suggestions/prompts/categories/prioritization.md +37 -0
  234. vidbyte_cli/services/suggestions/prompts/categories/product_experience.md +37 -0
  235. vidbyte_cli/services/suggestions/prompts/categories/quick_wins.md +37 -0
  236. vidbyte_cli/services/suggestions/prompts/categories/reframing.md +37 -0
  237. vidbyte_cli/services/suggestions/prompts/categories/risk_prevention.md +37 -0
  238. vidbyte_cli/services/suggestions/prompts/categories/simplification.md +37 -0
  239. vidbyte_cli/services/suggestions/prompts/categories/stop_or_defer.md +37 -0
  240. vidbyte_cli/services/suggestions/prompts/categories/strategy.md +37 -0
  241. vidbyte_cli/services/suggestions/prompts/categories/technical_possibilities.md +37 -0
  242. vidbyte_cli/services/suggestions/prompts/categories/verification.md +37 -0
  243. vidbyte_cli/services/suggestions/prompts/critic.md +209 -0
  244. vidbyte_cli/services/suggestions/prompts/critic_message.md +8 -0
  245. vidbyte_cli/services/suggestions/prompts/generator.md +64 -0
  246. vidbyte_cli/services/suggestions/prompts/library.py +72 -0
  247. vidbyte_cli/services/suggestions/prompts/message_generator_argument.md +1 -0
  248. vidbyte_cli/services/suggestions/prompts/message_generator_tool.md +1 -0
  249. vidbyte_cli/services/suggestions/prompts/message_parent_argument.md +1 -0
  250. vidbyte_cli/services/suggestions/prompts/message_parent_tool.md +1 -0
  251. vidbyte_cli/services/suggestions/prompts/message_receipt.md +1 -0
  252. vidbyte_cli/services/suggestions/prompts/revision.md +8 -0
  253. vidbyte_cli/services/suggestions/sdk.py +228 -0
  254. vidbyte_cli/services/suggestions/selection.py +78 -0
  255. vidbyte_cli/services/suggestions/service.py +540 -0
  256. vidbyte_cli/types/__init__.py +0 -0
  257. vidbyte_cli/types/api.py +101 -0
  258. vidbyte_cli/types/attachments.py +79 -0
  259. vidbyte_cli/types/ensemble.py +189 -0
  260. vidbyte_cli/types/provider.py +62 -0
  261. vidbyte_cli/types/research.py +260 -0
  262. vidbyte_cli/types/runtime.py +1098 -0
  263. vidbyte_cli/types/suggestions.py +518 -0
  264. vidbyte_cli-0.1.0.dist-info/METADATA +335 -0
  265. vidbyte_cli-0.1.0.dist-info/RECORD +269 -0
  266. vidbyte_cli-0.1.0.dist-info/WHEEL +5 -0
  267. vidbyte_cli-0.1.0.dist-info/entry_points.txt +2 -0
  268. vidbyte_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  269. vidbyte_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,113 @@
1
+ """Typed HTTP operations for local-runtime discovery and paid admission.
2
+
3
+ No execution input crosses this boundary. The future executor will call admission only
4
+ after it knows that a supported native host can actually be launched.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from ....types.runtime import (
12
+ RuntimeAdmissionGrant as AdmissionGrant,
13
+ )
14
+ from ....types.runtime import (
15
+ RuntimeAdmissionRequest as AdmissionRequest,
16
+ )
17
+ from ....types.runtime import (
18
+ RuntimeCapabilityCatalog,
19
+ RuntimeGrantVerificationRequest,
20
+ )
21
+ from ..client import ApiClient
22
+ from ..response import ResponseDecoder, ResponseShape
23
+
24
+ if TYPE_CHECKING:
25
+ from ..runtime_payment import RuntimePayment
26
+
27
+ RUNTIME_CATALOG_PATH = "/api/x402/runtime"
28
+ ADVERSARIAL_TEAM_ADMISSION_PATH = "/api/x402/runtime/adversarial-team/admissions"
29
+ # Matches the capability declared in the backend x402 catalog (vidbyte PR #508), which
30
+ # names these routes "activate" rather than "admissions".
31
+ SAME_HOST_ENSEMBLE_ADMISSION_PATH = "/api/x402/runtime/same-host-ensemble/activate"
32
+ PERSISTENCE_ADMISSION_PATH = "/api/x402/runtime/persistence/activate"
33
+ TASK_BOARD_ADMISSION_PATH = "/api/x402/runtime/task-board/activate"
34
+ STAGES_ADMISSION_PATH = "/api/x402/runtime/stages/admissions"
35
+
36
+
37
+ class RuntimeEndpoints:
38
+ """Runtime operations bound to one authenticated API client."""
39
+
40
+ def __init__(self, client: ApiClient) -> None:
41
+ # Retains the invocation-owned client without opening a connection.
42
+ self._client = client
43
+
44
+ def verify_grant(self, request: RuntimeGrantVerificationRequest) -> AdmissionGrant:
45
+ # Authenticates the proof at the server without exposing its HMAC signing secret.
46
+ return self._client.post(
47
+ "/api/x402/runtime/grants/verify", request, AdmissionGrant, shape=ResponseShape.DIRECT
48
+ )
49
+
50
+ def list_capabilities(self) -> RuntimeCapabilityCatalog:
51
+ # Reads the direct runtime-only catalog document.
52
+ return self._client.get(
53
+ RUNTIME_CATALOG_PATH, RuntimeCapabilityCatalog, shape=ResponseShape.DIRECT
54
+ )
55
+
56
+ def admit_adversarial_team(self, request: AdmissionRequest, key: str) -> AdmissionGrant:
57
+ # Purchases one replay-safe local execution admission.
58
+ return self._client.post(
59
+ ADVERSARIAL_TEAM_ADMISSION_PATH,
60
+ request,
61
+ AdmissionGrant,
62
+ shape=ResponseShape.DIRECT,
63
+ idempotency_key=key,
64
+ )
65
+
66
+ def admit_same_host_ensemble(self, request: AdmissionRequest, key: str) -> AdmissionGrant:
67
+ # Purchases one replay-safe local execution admission.
68
+ return self._client.post(
69
+ SAME_HOST_ENSEMBLE_ADMISSION_PATH,
70
+ request,
71
+ AdmissionGrant,
72
+ shape=ResponseShape.DIRECT,
73
+ idempotency_key=key,
74
+ )
75
+
76
+ def admit_persistence_x402(
77
+ self, request: AdmissionRequest, key: str, payer: RuntimePayment
78
+ ) -> AdmissionGrant:
79
+ # The same receipt goes through database-backed online verification before launch.
80
+ response = self._client.post_runtime_payment(
81
+ PERSISTENCE_ADMISSION_PATH, request, key, payer
82
+ )
83
+ return ResponseDecoder().one(response, AdmissionGrant, ResponseShape.DIRECT)
84
+
85
+ def admit_persistence(self, request: AdmissionRequest, key: str) -> AdmissionGrant:
86
+ # Purchases one replay-safe local execution admission.
87
+ return self._client.post(
88
+ PERSISTENCE_ADMISSION_PATH,
89
+ request,
90
+ AdmissionGrant,
91
+ shape=ResponseShape.DIRECT,
92
+ idempotency_key=key,
93
+ )
94
+
95
+ def admit_task_board(self, request: AdmissionRequest, key: str) -> AdmissionGrant:
96
+ # Purchases one replay-safe local task-board admission.
97
+ return self._client.post(
98
+ TASK_BOARD_ADMISSION_PATH,
99
+ request,
100
+ AdmissionGrant,
101
+ shape=ResponseShape.DIRECT,
102
+ idempotency_key=key,
103
+ )
104
+
105
+ def admit_stages(self, request: AdmissionRequest, key: str) -> AdmissionGrant:
106
+ # Purchases one replay-safe local stages admission at the one-cent floor.
107
+ return self._client.post(
108
+ STAGES_ADMISSION_PATH,
109
+ request,
110
+ AdmissionGrant,
111
+ shape=ResponseShape.DIRECT,
112
+ idempotency_key=key,
113
+ )
@@ -0,0 +1,86 @@
1
+ """One HTTP failure status in, one typed CliError out.
2
+
3
+ Classification reads the status code and the `x-request-id` header, and nothing else. The
4
+ response body is never parsed: failure prose in this CLI is static authored text, and a
5
+ backend body may quote a prompt, a credential, or another account's content.
6
+
7
+ The arms are split more finely than the status families suggest, because the remedies
8
+ differ. 401 means log in, 403 means grant a scope, 429 means poll less often, and a 409 on
9
+ this surface always means the same thing about continuing a run.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import httpx
15
+ from pydantic import JsonValue, ValidationError
16
+
17
+ from ...types.api import ApiUsageExhaustedProblem
18
+ from ..errors.cli_error import CliError
19
+ from ..errors.failures import (
20
+ ApiCredentialsRejected,
21
+ ApiCreditExhausted,
22
+ ApiOperationFailed,
23
+ ApiPermissionDenied,
24
+ ApiRateLimited,
25
+ ApiRequestConflicted,
26
+ ApiRequestRejected,
27
+ ApiResourceNotFound,
28
+ ApiRouteMissing,
29
+ ApiUnavailable,
30
+ ApiUnreachable,
31
+ )
32
+ from .response import ResponseDecoder
33
+
34
+
35
+ class ApiProblemMapper:
36
+ """Status-driven, body-free classification of backend failures."""
37
+
38
+ def from_response(
39
+ self,
40
+ response: httpx.Response,
41
+ *,
42
+ route_not_found: bool = False,
43
+ ) -> CliError:
44
+ # Maps one non-success status onto the failure whose hint actually resolves it.
45
+ request_id = ResponseDecoder.request_id(response)
46
+ status = response.status_code
47
+ match status:
48
+ case 401:
49
+ return ApiCredentialsRejected(request_id)
50
+ case 403:
51
+ return ApiPermissionDenied(request_id)
52
+ case 402:
53
+ return ApiCreditExhausted(request_id, self._usage_remediation(response))
54
+ case 404:
55
+ if route_not_found:
56
+ return ApiRouteMissing(request_id)
57
+ return ApiResourceNotFound(request_id)
58
+ case 409:
59
+ return ApiRequestConflicted(request_id)
60
+ case 400 | 422:
61
+ return ApiRequestRejected(request_id)
62
+ case 429:
63
+ return ApiRateLimited(request_id, self._retry_after(response))
64
+ case _ if status >= 500:
65
+ return ApiUnavailable(request_id)
66
+ case _:
67
+ return ApiOperationFailed(request_id)
68
+
69
+ def from_transport(self, error: httpx.HTTPError) -> CliError:
70
+ # No response ever arrived, so there is no status and no request ID to report.
71
+ return ApiUnreachable(error)
72
+
73
+ def _retry_after(self, response: httpx.Response) -> int | None:
74
+ # The authored rate-limit hint may repeat only a bounded numeric delta.
75
+ value = response.headers.get("retry-after", "").strip()
76
+ return int(value) if value.isdigit() else None
77
+
78
+ def _usage_remediation(self, response: httpx.Response) -> dict[str, JsonValue] | None:
79
+ # Only the bounded, allowlisted exhaustion contract may influence CLI output.
80
+ if len(response.content) > 32_768:
81
+ return None
82
+ try:
83
+ problem = ApiUsageExhaustedProblem.model_validate_json(response.content)
84
+ except (ValidationError, ValueError):
85
+ return None
86
+ return problem.remediation.model_dump(mode="json")
@@ -0,0 +1,101 @@
1
+ """What a backend route's success body looks like, and the only decoder that reads one.
2
+
3
+ Routes disagree about their envelope: the research surface returns its DTO directly, while
4
+ the older public resources wrap it in `{success, data}`. The shape is therefore declared per
5
+ route by the endpoint group rather than assumed here, so adding a route cannot silently
6
+ unwrap something that was never wrapped.
7
+
8
+ Every rejection is the same failure on purpose. A caller cannot act differently on a wrong
9
+ content type than on a schema mismatch, and telling them apart would mean quoting the body.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from enum import StrEnum
16
+ from typing import TYPE_CHECKING, TypeVar
17
+
18
+ from pydantic import BaseModel, ValidationError
19
+
20
+ from ..errors.failures import ApiResponseUnsupported
21
+
22
+ if TYPE_CHECKING:
23
+ import httpx
24
+
25
+ TModel = TypeVar("TModel", bound=BaseModel)
26
+ # Bounds a hostile or misrouted response before it is parsed into memory.
27
+ _MAX_RESPONSE_BYTES = 1_048_576
28
+
29
+
30
+ class ResponseShape(StrEnum):
31
+ """How a route wraps the payload a caller actually wants.
32
+
33
+ The research surface returns its DTO directly; the older public resources wrap it in
34
+ `{success, data}`. Every route declares which, so adding one cannot silently unwrap
35
+ something that was never wrapped.
36
+ """
37
+
38
+ DIRECT = "direct"
39
+ ENVELOPE = "envelope"
40
+
41
+
42
+ class ResponseDecoder:
43
+ """Bounded validation from an HTTP success response into typed models."""
44
+
45
+ def one(
46
+ self,
47
+ response: httpx.Response,
48
+ model: type[TModel],
49
+ shape: ResponseShape,
50
+ ) -> TModel:
51
+ # Decodes a single record, unwrapping the envelope only when the route declares one.
52
+ payload = self._payload(response)
53
+ if shape is not ResponseShape.DIRECT:
54
+ payload = self._unwrap(response, payload)
55
+ try:
56
+ return model.model_validate(payload)
57
+ except ValidationError as error:
58
+ raise self._unsupported(response, error) from error
59
+
60
+ def _payload(self, response: httpx.Response) -> object:
61
+ # Content type and both size bounds are checked before any parsing happens.
62
+ if response.status_code == 204:
63
+ raise self._unsupported(response)
64
+ content_type = response.headers.get("content-type", "").split(";", 1)[0].lower()
65
+ if content_type != "application/json" and not content_type.endswith("+json"):
66
+ raise self._unsupported(response)
67
+ # A chunked response declares no length, so the materialized body is bounded too.
68
+ declared = response.headers.get("content-length")
69
+ if declared is not None and declared.isdigit() and int(declared) > _MAX_RESPONSE_BYTES:
70
+ raise self._unsupported(response)
71
+ content = response.content
72
+ if not content or len(content) > _MAX_RESPONSE_BYTES:
73
+ raise self._unsupported(response)
74
+ try:
75
+ return json.loads(content)
76
+ except (UnicodeError, json.JSONDecodeError) as error:
77
+ raise self._unsupported(response, error) from error
78
+
79
+ def _unwrap(self, response: httpx.Response, payload: object) -> object:
80
+ # The `{success, data}` wrapper the older public resources return.
81
+ if not isinstance(payload, dict) or payload.get("success") is False:
82
+ raise self._unsupported(response)
83
+ data = payload.get("data")
84
+ if data is None:
85
+ raise self._unsupported(response)
86
+ return data
87
+
88
+ def _unsupported(
89
+ self,
90
+ response: httpx.Response,
91
+ cause: Exception | None = None,
92
+ ) -> ApiResponseUnsupported:
93
+ return ApiResponseUnsupported(self.request_id(response), cause)
94
+
95
+ @staticmethod
96
+ def request_id(response: httpx.Response) -> str | None:
97
+ # A correlation header, never a body field: only headers are safe to echo back.
98
+ value = response.headers.get("x-request-id")
99
+ if value is None or not 1 <= len(value) <= 128:
100
+ return None
101
+ return str(value)
@@ -0,0 +1,116 @@
1
+ """Whether one failed attempt may be repeated, and how long to wait first.
2
+
3
+ Kept apart from the client so the rule is readable on its own: retrying a mutation is only
4
+ safe while an idempotency key makes the backend collapse duplicates, and every research
5
+ mutation is priced. A `POST` without that key is therefore never repeated, whatever failed.
6
+
7
+ This class decides; it never sleeps. The caller owns the wait, so the decision stays pure
8
+ apart from injected randomness.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import email.utils
14
+ import random
15
+ from dataclasses import dataclass
16
+ from datetime import UTC, datetime
17
+
18
+ import httpx
19
+
20
+ _MAX_ATTEMPTS = 3
21
+ _MAXIMUM_DELAY_SECONDS = 10.0
22
+ _BASE_DELAY_SECONDS = 0.25
23
+ _UNJITTERED_CAP_SECONDS = 4.0
24
+ _JITTER_SECONDS = 0.25
25
+ _SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})
26
+ _RETRYABLE_STATUSES = frozenset({408, 429, 502, 503, 504})
27
+ _RETRYABLE_ERRORS = (
28
+ httpx.ConnectError,
29
+ httpx.ConnectTimeout,
30
+ httpx.ReadTimeout,
31
+ httpx.RemoteProtocolError,
32
+ )
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class RequestMetadata:
37
+ """The two facts about a request that decide whether repeating it is safe."""
38
+
39
+ method: str
40
+ has_idempotency_key: bool = False
41
+
42
+
43
+ # One attempt ends as exactly one of these, which is why they travel as a union rather than
44
+ # as a pair of nullable arguments nobody can see are mutually exclusive.
45
+ RequestOutcome = httpx.Response | httpx.HTTPError
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class RetryDecision:
50
+ """One verdict, plus the wait the caller should perform before the next attempt."""
51
+
52
+ retry: bool
53
+ delay_seconds: float = 0.0
54
+ delay_clamped: bool = False
55
+
56
+
57
+ class RetryPolicy:
58
+ """Bounded safe-retry policy with exponential backoff and jitter."""
59
+
60
+ def __init__(self, random_source: random.Random | None = None) -> None:
61
+ # Randomness is injected so jitter can be made deterministic by a caller.
62
+ self._random = random_source or random.Random()
63
+
64
+ def decide(
65
+ self,
66
+ request: RequestMetadata,
67
+ attempt: int,
68
+ outcome: RequestOutcome,
69
+ ) -> RetryDecision:
70
+ # Answers whether this attempt may be repeated, and after how long.
71
+ if attempt >= _MAX_ATTEMPTS or not self._is_repeatable(request):
72
+ return RetryDecision(False)
73
+ if not self._is_transient(outcome):
74
+ return RetryDecision(False)
75
+ return self._delay(attempt, outcome)
76
+
77
+ def _is_repeatable(self, request: RequestMetadata) -> bool:
78
+ # A priced mutation may only be repeated while a key makes the backend deduplicate.
79
+ method = request.method.upper()
80
+ return method in _SAFE_METHODS or (method == "POST" and request.has_idempotency_key)
81
+
82
+ def _is_transient(self, outcome: RequestOutcome) -> bool:
83
+ # Distinguishes a failure worth repeating from a rejection that will repeat itself.
84
+ if isinstance(outcome, httpx.Response):
85
+ return outcome.status_code in _RETRYABLE_STATUSES
86
+ return isinstance(outcome, _RETRYABLE_ERRORS)
87
+
88
+ def _delay(self, attempt: int, outcome: RequestOutcome) -> RetryDecision:
89
+ # A server's own pacing wins over the local curve, up to the local ceiling.
90
+ server_delay = self._retry_after(outcome)
91
+ if server_delay is not None:
92
+ clamped = server_delay > _MAXIMUM_DELAY_SECONDS
93
+ return RetryDecision(True, min(server_delay, _MAXIMUM_DELAY_SECONDS), clamped)
94
+ backoff = min(_BASE_DELAY_SECONDS * (2 ** (attempt - 1)), _UNJITTERED_CAP_SECONDS)
95
+ jittered = backoff + self._random.uniform(0.0, _JITTER_SECONDS)
96
+ return RetryDecision(True, min(jittered, _MAXIMUM_DELAY_SECONDS))
97
+
98
+ def _retry_after(self, outcome: RequestOutcome) -> float | None:
99
+ # Accepts both header forms; an unparseable value is ignored rather than read as 0.
100
+ if not isinstance(outcome, httpx.Response):
101
+ return None
102
+ value = outcome.headers.get("retry-after")
103
+ if value is None:
104
+ return None
105
+ try:
106
+ return max(0.0, float(value))
107
+ except ValueError:
108
+ return self._retry_after_date(value)
109
+
110
+ def _retry_after_date(self, value: str) -> float | None:
111
+ # An HTTP-date already in the past yields no wait rather than a negative one.
112
+ try:
113
+ parsed = email.utils.parsedate_to_datetime(value)
114
+ except (TypeError, ValueError):
115
+ return None
116
+ return max(0.0, (parsed.astimezone(UTC) - datetime.now(UTC)).total_seconds())
@@ -0,0 +1,89 @@
1
+ """Explicit, bounded x402 authorization for one runtime admission.
2
+
3
+ The API client owns HTTP and retries. This object validates the challenge and signs
4
+ once; it never funds a wallet, launches an agent, or logs payment credentials.
5
+ """
6
+
7
+ from collections.abc import Mapping
8
+ from urllib.parse import urlsplit
9
+
10
+ import httpx
11
+ from eth_account import Account
12
+ from eth_utils.address import is_address
13
+ from x402 import x402ClientSync
14
+ from x402.http import x402HTTPClientSync
15
+ from x402.mechanisms.evm.constants import NETWORK_CONFIGS
16
+ from x402.mechanisms.evm.exact import register_exact_evm_client
17
+ from x402.schemas import PaymentRequired, PaymentRequirements
18
+
19
+ from ..constants.runtime import RuntimePaymentConfig as Config
20
+ from ..errors.failures import RuntimePaymentFailed
21
+
22
+
23
+ class RuntimePayment:
24
+ """Invocation-owned signer with an exact price and allowed network."""
25
+
26
+ def __init__(self, environment: Mapping[str, str], cents: int) -> None:
27
+ # Validate opt-in credentials before sending the admission request.
28
+ self._network = environment.get(Config.NETWORK_ENV, Config.DEFAULT_NETWORK)
29
+ self._cents = cents
30
+ self._signed = False
31
+ if self._network not in Config.NETWORKS or cents <= 0:
32
+ raise RuntimePaymentFailed("configuration_invalid")
33
+ try:
34
+ signer = Account.from_key(environment.get(Config.PRIVATE_KEY_ENV, ""))
35
+ client = register_exact_evm_client(x402ClientSync(), signer, self._network)
36
+ self._http = x402HTTPClientSync(client)
37
+ except Exception as error:
38
+ raise RuntimePaymentFailed("signer_unavailable") from error
39
+
40
+ def headers(self, response: httpx.Response) -> dict[str, str]:
41
+ # Never create another authorization after a paid attempt fails or times out.
42
+ if self._signed:
43
+ raise RuntimePaymentFailed("authorization_already_created")
44
+ header = response.headers.get("PAYMENT-REQUIRED", "")
45
+ if not header or len(header) > Config.MAX_CHALLENGE_CHARACTERS:
46
+ raise RuntimePaymentFailed("challenge_missing_or_oversized")
47
+ try:
48
+ required = self._http.get_payment_required_response(response.headers.get)
49
+ self._validate(required, str(response.request.url))
50
+ self._signed = True
51
+ payload = self._http.create_payment_payload(required)
52
+ return self._http.encode_payment_signature_header(payload)
53
+ except RuntimePaymentFailed:
54
+ raise
55
+ except Exception as error:
56
+ raise RuntimePaymentFailed("challenge_or_signing_failed") from error
57
+
58
+ def _validate(self, required: object, url: str) -> None:
59
+ # Bind the authorization to the requested origin/resource and one reviewed exact price.
60
+ if not isinstance(required, PaymentRequired) or required.x402_version != 2:
61
+ raise RuntimePaymentFailed("protocol_invalid")
62
+ if required.resource is None or len(required.accepts) != 1:
63
+ raise RuntimePaymentFailed("resource_or_options_invalid")
64
+ resource, requested = urlsplit(required.resource.url), urlsplit(url)
65
+ if resource.scheme or resource.netloc:
66
+ matches = resource == requested
67
+ else:
68
+ matches = (
69
+ resource.path == requested.path and not resource.query and not resource.fragment
70
+ )
71
+ if not matches:
72
+ raise RuntimePaymentFailed("resource_mismatch")
73
+ self._validate_requirement(required.accepts[0])
74
+
75
+ def _validate_requirement(self, item: PaymentRequirements) -> None:
76
+ # Canonical six-decimal USDC and EIP-712 metadata prevent arbitrary-token authorization.
77
+ asset = NETWORK_CONFIGS[self._network]["default_asset"]
78
+ expected = str(self._cents * Config.MICROUNITS_PER_CENT)
79
+ if item.network != self._network or item.scheme != "exact" or item.amount != expected:
80
+ raise RuntimePaymentFailed("price_or_network_mismatch")
81
+ if item.asset.lower() != asset["address"].lower():
82
+ raise RuntimePaymentFailed("asset_mismatch")
83
+ extra = item.extra or {}
84
+ if extra.get("name") != asset["name"] or extra.get("version") != asset["version"]:
85
+ raise RuntimePaymentFailed("token_domain_mismatch")
86
+ if not is_address(item.pay_to) or int(item.pay_to, 16) == 0:
87
+ raise RuntimePaymentFailed("recipient_invalid")
88
+ if not 0 < item.max_timeout_seconds <= Config.MAX_AUTHORIZATION_SECONDS:
89
+ raise RuntimePaymentFailed("authorization_duration_invalid")
@@ -0,0 +1,16 @@
1
+ """Secret-safe credential resolution and storage contracts."""
2
+
3
+ from .credentials import Credentials, CredentialStorage, CredentialStore
4
+ from .resolver import CredentialResolver, CredentialSource, ResolvedCredential
5
+ from .verifier import ApiCredentialVerifier, CredentialVerifier
6
+
7
+ __all__ = [
8
+ "ApiCredentialVerifier",
9
+ "CredentialResolver",
10
+ "CredentialSource",
11
+ "CredentialStorage",
12
+ "CredentialStore",
13
+ "CredentialVerifier",
14
+ "Credentials",
15
+ "ResolvedCredential",
16
+ ]