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.
- vidbyte_cli/__init__.py +11 -0
- vidbyte_cli/__main__.py +6 -0
- vidbyte_cli/cli.py +18 -0
- vidbyte_cli/commands/__init__.py +86 -0
- vidbyte_cli/commands/agent_options.py +53 -0
- vidbyte_cli/commands/agents/__init__.py +31 -0
- vidbyte_cli/commands/agents/suggestion/__init__.py +38 -0
- vidbyte_cli/commands/agents/suggestion/prompts/__init__.py +3 -0
- vidbyte_cli/commands/agents/suggestion/prompts/all_categories.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/approaches.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/avoid.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/blockers.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/capability.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/categories.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/category.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/completed.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/constraint.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/context.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/count.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/critic_model.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/decision.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/dry_run.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/extra_compute.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_accept.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_group.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_project.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_reason.md +98 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_reject.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_suggestion.md +101 -0
- vidbyte_cli/commands/agents/suggestion/prompts/files.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/forbidden.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/goal.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/handoff.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/horizon.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/hypotheses.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/idea.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/in_progress.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/input.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/library.py +29 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_messages.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_output_tokens.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_total_tokens.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/mistakes.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/outcomes.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_create.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_description.md +98 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_group.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_key.md +96 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_list.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_title.md +99 -0
- vidbyte_cli/commands/agents/suggestion/prompts/provider.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/question.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/risks.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/rounds.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/run.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/success.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/timeout.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/trajectory.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/view.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/view_all.md +5 -0
- vidbyte_cli/commands/agents/suggestion/render.py +77 -0
- vidbyte_cli/commands/agents/suggestion/request_builder.py +240 -0
- vidbyte_cli/commands/agents/suggestion/suggest.py +154 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_categories.py +77 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_feedback.py +85 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_handoff.py +67 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_projects.py +95 -0
- vidbyte_cli/commands/auth/__init__.py +0 -0
- vidbyte_cli/commands/auth/login.py +100 -0
- vidbyte_cli/commands/auth/logout.py +41 -0
- vidbyte_cli/commands/auth/whoami.py +51 -0
- vidbyte_cli/commands/billing/__init__.py +20 -0
- vidbyte_cli/commands/billing/prompts/__init__.py +1 -0
- vidbyte_cli/commands/billing/prompts/confirm.md +16 -0
- vidbyte_cli/commands/billing/prompts/group.md +13 -0
- vidbyte_cli/commands/billing/prompts/idempotency_key.md +16 -0
- vidbyte_cli/commands/billing/prompts/library.py +18 -0
- vidbyte_cli/commands/billing/prompts/top_up.md +16 -0
- vidbyte_cli/commands/billing/top_up.py +65 -0
- vidbyte_cli/commands/config/__init__.py +0 -0
- vidbyte_cli/commands/config/get.py +45 -0
- vidbyte_cli/commands/config/set.py +39 -0
- vidbyte_cli/commands/provider/__init__.py +9 -0
- vidbyte_cli/commands/provider/login.py +102 -0
- vidbyte_cli/commands/provider/logout.py +45 -0
- vidbyte_cli/commands/provider/whoami.py +49 -0
- vidbyte_cli/commands/research/__init__.py +5 -0
- vidbyte_cli/commands/research/add.py +41 -0
- vidbyte_cli/commands/research/options.py +134 -0
- vidbyte_cli/commands/research/render.py +157 -0
- vidbyte_cli/commands/research/resume.py +37 -0
- vidbyte_cli/commands/research/start.py +37 -0
- vidbyte_cli/commands/research/status.py +32 -0
- vidbyte_cli/commands/research/thread.py +30 -0
- vidbyte_cli/commands/research/threads.py +35 -0
- vidbyte_cli/commands/research/watch.py +93 -0
- vidbyte_cli/commands/runtime/__init__.py +23 -0
- vidbyte_cli/commands/runtime/adversarial_team.py +41 -0
- vidbyte_cli/commands/runtime/doctor.py +44 -0
- vidbyte_cli/commands/runtime/list.py +47 -0
- vidbyte_cli/commands/runtime/persistence.py +141 -0
- vidbyte_cli/commands/runtime/same_host_ensemble.py +221 -0
- vidbyte_cli/commands/runtime/stages.py +553 -0
- vidbyte_cli/commands/runtime/task_board.py +1342 -0
- vidbyte_cli/commands/setup/__init__.py +0 -0
- vidbyte_cli/commands/setup/doctor.py +64 -0
- vidbyte_cli/commands/setup/prompts/__init__.py +1 -0
- vidbyte_cli/commands/setup/prompts/browser.md +7 -0
- vidbyte_cli/commands/setup/prompts/library.py +18 -0
- vidbyte_cli/commands/setup/prompts/star.md +13 -0
- vidbyte_cli/commands/setup/star.py +132 -0
- vidbyte_cli/lib/__init__.py +0 -0
- vidbyte_cli/lib/api/__init__.py +0 -0
- vidbyte_cli/lib/api/client.py +236 -0
- vidbyte_cli/lib/api/endpoints/__init__.py +0 -0
- vidbyte_cli/lib/api/endpoints/auth.py +24 -0
- vidbyte_cli/lib/api/endpoints/billing.py +36 -0
- vidbyte_cli/lib/api/endpoints/research.py +103 -0
- vidbyte_cli/lib/api/endpoints/runtime.py +113 -0
- vidbyte_cli/lib/api/problem.py +86 -0
- vidbyte_cli/lib/api/response.py +101 -0
- vidbyte_cli/lib/api/retry.py +116 -0
- vidbyte_cli/lib/api/runtime_payment.py +89 -0
- vidbyte_cli/lib/auth/__init__.py +16 -0
- vidbyte_cli/lib/auth/credentials.py +223 -0
- vidbyte_cli/lib/auth/input.py +59 -0
- vidbyte_cli/lib/auth/keyring_store.py +126 -0
- vidbyte_cli/lib/auth/provider_credentials.py +157 -0
- vidbyte_cli/lib/auth/provider_input.py +54 -0
- vidbyte_cli/lib/auth/provider_resolver.py +89 -0
- vidbyte_cli/lib/auth/provider_store.py +149 -0
- vidbyte_cli/lib/auth/provider_verifier.py +219 -0
- vidbyte_cli/lib/auth/resolver.py +63 -0
- vidbyte_cli/lib/auth/verifier.py +38 -0
- vidbyte_cli/lib/config/__init__.py +25 -0
- vidbyte_cli/lib/config/atomic.py +47 -0
- vidbyte_cli/lib/config/config.py +141 -0
- vidbyte_cli/lib/config/migration.py +81 -0
- vidbyte_cli/lib/config/models.py +145 -0
- vidbyte_cli/lib/config/paths.py +66 -0
- vidbyte_cli/lib/config/resolver.py +137 -0
- vidbyte_cli/lib/constants/__init__.py +1 -0
- vidbyte_cli/lib/constants/runtime.py +186 -0
- vidbyte_cli/lib/errors/__init__.py +12 -0
- vidbyte_cli/lib/errors/cli_error.py +86 -0
- vidbyte_cli/lib/errors/codes.py +43 -0
- vidbyte_cli/lib/errors/failures.py +2910 -0
- vidbyte_cli/lib/errors/handler.py +72 -0
- vidbyte_cli/lib/files/__init__.py +6 -0
- vidbyte_cli/lib/files/documents.py +94 -0
- vidbyte_cli/lib/files/store.py +156 -0
- vidbyte_cli/lib/io/__init__.py +16 -0
- vidbyte_cli/lib/io/attachments.py +129 -0
- vidbyte_cli/lib/io/codex_attachments.py +47 -0
- vidbyte_cli/lib/io/prompt.py +71 -0
- vidbyte_cli/lib/io/streams.py +37 -0
- vidbyte_cli/lib/io/terminal.py +66 -0
- vidbyte_cli/lib/output/__init__.py +7 -0
- vidbyte_cli/lib/output/formats.py +29 -0
- vidbyte_cli/lib/output/manager.py +95 -0
- vidbyte_cli/lib/output/models.py +49 -0
- vidbyte_cli/lib/runtime/__init__.py +9 -0
- vidbyte_cli/lib/runtime/application.py +134 -0
- vidbyte_cli/lib/runtime/context.py +250 -0
- vidbyte_cli/lib/runtime/options.py +159 -0
- vidbyte_cli/lib/runtime/version.py +30 -0
- vidbyte_cli/lib/runtime_primitives/__init__.py +28 -0
- vidbyte_cli/lib/runtime_primitives/executor.py +54 -0
- vidbyte_cli/lib/runtime_primitives/gate.py +105 -0
- vidbyte_cli/lib/runtime_primitives/hosts.py +45 -0
- vidbyte_cli/lib/runtime_primitives/planner.py +79 -0
- vidbyte_cli/lib/runtime_primitives/stages.py +338 -0
- vidbyte_cli/lib/runtime_primitives/task_board.py +982 -0
- vidbyte_cli/lib/runtime_primitives/task_board_checkpoints.py +473 -0
- vidbyte_cli/lib/runtime_primitives/task_board_system.md +33 -0
- vidbyte_cli/lib/runtime_primitives/task_board_tasks.py +111 -0
- vidbyte_cli/lib/runtime_primitives/verification.py +68 -0
- vidbyte_cli/services/__init__.py +5 -0
- vidbyte_cli/services/ensemble/__init__.py +7 -0
- vidbyte_cli/services/ensemble/prompts/__init__.py +6 -0
- vidbyte_cli/services/ensemble/prompts/implementer_system.md +546 -0
- vidbyte_cli/services/ensemble/prompts/implementer_turn.md +49 -0
- vidbyte_cli/services/ensemble/prompts/library.py +188 -0
- vidbyte_cli/services/ensemble/prompts/planner_system.md +728 -0
- vidbyte_cli/services/ensemble/prompts/planner_turn.md +6 -0
- vidbyte_cli/services/ensemble/prompts/role_system.md +104 -0
- vidbyte_cli/services/ensemble/prompts/role_turn.md +19 -0
- vidbyte_cli/services/ensemble/prompts/selector_final_turn.md +19 -0
- vidbyte_cli/services/ensemble/prompts/selector_round_turn.md +17 -0
- vidbyte_cli/services/ensemble/prompts/selector_system.md +89 -0
- vidbyte_cli/services/ensemble/runner.py +30 -0
- vidbyte_cli/services/ensemble/sdk.py +134 -0
- vidbyte_cli/services/ensemble/service.py +233 -0
- vidbyte_cli/services/ensemble/settings.py +60 -0
- vidbyte_cli/services/persistence/__init__.py +6 -0
- vidbyte_cli/services/persistence/prompts/__init__.py +6 -0
- vidbyte_cli/services/persistence/prompts/library.py +46 -0
- vidbyte_cli/services/persistence/prompts/persistence_system.md +1 -0
- vidbyte_cli/services/persistence/prompts/persistence_turn.md +3 -0
- vidbyte_cli/services/persistence/runner.py +35 -0
- vidbyte_cli/services/persistence/session.py +116 -0
- vidbyte_cli/services/suggestions/__init__.py +7 -0
- vidbyte_cli/services/suggestions/categories.py +270 -0
- vidbyte_cli/services/suggestions/context.py +222 -0
- vidbyte_cli/services/suggestions/extra_compute.py +53 -0
- vidbyte_cli/services/suggestions/handoff.py +173 -0
- vidbyte_cli/services/suggestions/message_tools.py +47 -0
- vidbyte_cli/services/suggestions/project.py +253 -0
- vidbyte_cli/services/suggestions/prompts/__init__.py +1 -0
- vidbyte_cli/services/suggestions/prompts/categories/__init__.py +3 -0
- vidbyte_cli/services/suggestions/prompts/categories/adjacent_opportunity.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/alternative.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/big_bets.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/bottleneck.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/business_growth.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/completion.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/continuation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/coordination.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/creative_exploration.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/cross_domain.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/delegation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/experiment.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/feedback.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/goal_clarification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/immediate_next_steps.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/investigation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/learning.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/leverage.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/long_term_directions.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/preparation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/prerequisite.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/prioritization.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/product_experience.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/quick_wins.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/reframing.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/risk_prevention.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/simplification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/stop_or_defer.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/strategy.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/technical_possibilities.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/verification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/critic.md +209 -0
- vidbyte_cli/services/suggestions/prompts/critic_message.md +8 -0
- vidbyte_cli/services/suggestions/prompts/generator.md +64 -0
- vidbyte_cli/services/suggestions/prompts/library.py +72 -0
- vidbyte_cli/services/suggestions/prompts/message_generator_argument.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_generator_tool.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_parent_argument.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_parent_tool.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_receipt.md +1 -0
- vidbyte_cli/services/suggestions/prompts/revision.md +8 -0
- vidbyte_cli/services/suggestions/sdk.py +228 -0
- vidbyte_cli/services/suggestions/selection.py +78 -0
- vidbyte_cli/services/suggestions/service.py +540 -0
- vidbyte_cli/types/__init__.py +0 -0
- vidbyte_cli/types/api.py +101 -0
- vidbyte_cli/types/attachments.py +79 -0
- vidbyte_cli/types/ensemble.py +189 -0
- vidbyte_cli/types/provider.py +62 -0
- vidbyte_cli/types/research.py +260 -0
- vidbyte_cli/types/runtime.py +1098 -0
- vidbyte_cli/types/suggestions.py +518 -0
- vidbyte_cli-0.1.0.dist-info/METADATA +335 -0
- vidbyte_cli-0.1.0.dist-info/RECORD +269 -0
- vidbyte_cli-0.1.0.dist-info/WHEEL +5 -0
- vidbyte_cli-0.1.0.dist-info/entry_points.txt +2 -0
- vidbyte_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
]
|