verdifax 0.2.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.
- verdifax/__init__.py +147 -0
- verdifax/_transport.py +61 -0
- verdifax/_validation.py +61 -0
- verdifax/_version.py +3 -0
- verdifax/async_client.py +208 -0
- verdifax/client.py +235 -0
- verdifax/exceptions.py +87 -0
- verdifax/helpers.py +105 -0
- verdifax/models.py +357 -0
- verdifax/py.typed +0 -0
- verdifax/section0.py +279 -0
- verdifax-0.2.0.dist-info/METADATA +137 -0
- verdifax-0.2.0.dist-info/RECORD +16 -0
- verdifax-0.2.0.dist-info/WHEEL +5 -0
- verdifax-0.2.0.dist-info/licenses/LICENSE +21 -0
- verdifax-0.2.0.dist-info/top_level.txt +1 -0
verdifax/__init__.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Verdifax — cryptographic attestation for AI inference.
|
|
2
|
+
|
|
3
|
+
Three-line integration:
|
|
4
|
+
|
|
5
|
+
>>> import verdifax
|
|
6
|
+
>>> receipt = verdifax.attest(
|
|
7
|
+
... payload="hello verdifax",
|
|
8
|
+
... program_id="a" * 64,
|
|
9
|
+
... route_id="route-test",
|
|
10
|
+
... registry_record_hash="b" * 64,
|
|
11
|
+
... ) # doctest: +SKIP
|
|
12
|
+
>>> receipt.manifest_hash # doctest: +SKIP
|
|
13
|
+
'7f...'
|
|
14
|
+
|
|
15
|
+
The SDK targets the Verdifax orchestrator REST API. By default it talks
|
|
16
|
+
to ``http://localhost:9090``; override with ``$VERDIFAX_API_URL`` or by
|
|
17
|
+
passing ``base_url`` to :class:`VerdifaxClient`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Optional as _Optional, Union as _Union
|
|
23
|
+
|
|
24
|
+
from ._version import __version__
|
|
25
|
+
from .async_client import AsyncVerdifaxClient
|
|
26
|
+
from .client import VerdifaxClient
|
|
27
|
+
from .exceptions import (
|
|
28
|
+
APIError,
|
|
29
|
+
ConnectionError,
|
|
30
|
+
StageError,
|
|
31
|
+
ValidationError,
|
|
32
|
+
VerdifaxError,
|
|
33
|
+
)
|
|
34
|
+
from .helpers import attest_claude_response, attest_openai_response
|
|
35
|
+
from .models import (
|
|
36
|
+
KERNEL_NAMES,
|
|
37
|
+
STAGE_NAMES,
|
|
38
|
+
AttestationReceipt,
|
|
39
|
+
AttestedContext,
|
|
40
|
+
ExecuteRequest,
|
|
41
|
+
ExecutionManifest,
|
|
42
|
+
StageResult,
|
|
43
|
+
)
|
|
44
|
+
from . import section0 # §0 cross-language canonical hash verifier
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def from_env(
|
|
48
|
+
base_url: _Optional[str] = None,
|
|
49
|
+
api_key: _Optional[str] = None,
|
|
50
|
+
timeout: _Optional[float] = None,
|
|
51
|
+
) -> VerdifaxClient:
|
|
52
|
+
"""Construct a :class:`VerdifaxClient` from environment variables.
|
|
53
|
+
|
|
54
|
+
Honors ``VERDIFAX_API_URL`` and ``VERDIFAX_API_KEY``. Any explicit
|
|
55
|
+
arguments override the corresponding environment variable.
|
|
56
|
+
"""
|
|
57
|
+
kwargs: dict[str, object] = {}
|
|
58
|
+
if base_url is not None:
|
|
59
|
+
kwargs["base_url"] = base_url
|
|
60
|
+
if api_key is not None:
|
|
61
|
+
kwargs["api_key"] = api_key
|
|
62
|
+
if timeout is not None:
|
|
63
|
+
kwargs["timeout"] = timeout
|
|
64
|
+
return VerdifaxClient(**kwargs) # type: ignore[arg-type]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def attest(
|
|
68
|
+
payload: _Union[str, bytes],
|
|
69
|
+
program_id: str,
|
|
70
|
+
route_id: str,
|
|
71
|
+
registry_record_hash: str,
|
|
72
|
+
*,
|
|
73
|
+
attested_context: _Optional[AttestedContext] = None,
|
|
74
|
+
base_url: _Optional[str] = None,
|
|
75
|
+
api_key: _Optional[str] = None,
|
|
76
|
+
timeout: _Optional[float] = None,
|
|
77
|
+
) -> AttestationReceipt:
|
|
78
|
+
"""Module-level shortcut: attest a payload using a one-shot client.
|
|
79
|
+
|
|
80
|
+
Equivalent to ``VerdifaxClient(...).attest(...)`` with the client
|
|
81
|
+
closed automatically afterwards. For high-throughput use, instantiate
|
|
82
|
+
a :class:`VerdifaxClient` yourself and reuse its connection pool.
|
|
83
|
+
|
|
84
|
+
Pass ``attested_context`` to record a caller-attested actor / model /
|
|
85
|
+
decision block into the run's EPA artifact — see
|
|
86
|
+
:class:`AttestedContext`.
|
|
87
|
+
"""
|
|
88
|
+
with from_env(base_url=base_url, api_key=api_key, timeout=timeout) as client:
|
|
89
|
+
return client.attest(
|
|
90
|
+
payload=payload,
|
|
91
|
+
program_id=program_id,
|
|
92
|
+
route_id=route_id,
|
|
93
|
+
registry_record_hash=registry_record_hash,
|
|
94
|
+
attested_context=attested_context,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def verify(
|
|
99
|
+
manifest_hash: str,
|
|
100
|
+
payload: _Union[str, bytes],
|
|
101
|
+
program_id: str,
|
|
102
|
+
route_id: str,
|
|
103
|
+
registry_record_hash: str,
|
|
104
|
+
*,
|
|
105
|
+
attested_context: _Optional[AttestedContext] = None,
|
|
106
|
+
base_url: _Optional[str] = None,
|
|
107
|
+
api_key: _Optional[str] = None,
|
|
108
|
+
timeout: _Optional[float] = None,
|
|
109
|
+
) -> bool:
|
|
110
|
+
"""Module-level shortcut: verify a previously issued manifest hash.
|
|
111
|
+
|
|
112
|
+
``attested_context`` must match the value used on the original
|
|
113
|
+
``attest`` call.
|
|
114
|
+
"""
|
|
115
|
+
with from_env(base_url=base_url, api_key=api_key, timeout=timeout) as client:
|
|
116
|
+
return client.verify(
|
|
117
|
+
manifest_hash=manifest_hash,
|
|
118
|
+
payload=payload,
|
|
119
|
+
program_id=program_id,
|
|
120
|
+
route_id=route_id,
|
|
121
|
+
registry_record_hash=registry_record_hash,
|
|
122
|
+
attested_context=attested_context,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__all__ = [
|
|
127
|
+
"APIError",
|
|
128
|
+
"AsyncVerdifaxClient",
|
|
129
|
+
"AttestationReceipt",
|
|
130
|
+
"AttestedContext",
|
|
131
|
+
"ConnectionError",
|
|
132
|
+
"ExecuteRequest",
|
|
133
|
+
"ExecutionManifest",
|
|
134
|
+
"KERNEL_NAMES",
|
|
135
|
+
"STAGE_NAMES",
|
|
136
|
+
"StageError",
|
|
137
|
+
"StageResult",
|
|
138
|
+
"ValidationError",
|
|
139
|
+
"VerdifaxClient",
|
|
140
|
+
"VerdifaxError",
|
|
141
|
+
"__version__",
|
|
142
|
+
"attest",
|
|
143
|
+
"attest_claude_response",
|
|
144
|
+
"attest_openai_response",
|
|
145
|
+
"from_env",
|
|
146
|
+
"verify",
|
|
147
|
+
]
|
verdifax/_transport.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Shared response handling for sync and async clients.
|
|
2
|
+
|
|
3
|
+
Centralizing this logic keeps :class:`VerdifaxClient` and
|
|
4
|
+
:class:`AsyncVerdifaxClient` from duplicating error-translation code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from .exceptions import APIError, ConnectionError as VerdifaxConnectionError, StageError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_response(response: httpx.Response) -> dict[str, Any]:
|
|
17
|
+
"""Validate an HTTP response and return its decoded JSON body.
|
|
18
|
+
|
|
19
|
+
Raises :class:`StageError` for orchestrator stage failures (HTTP 422
|
|
20
|
+
with an ``error_stage`` in the body), :class:`APIError` for any other
|
|
21
|
+
non-2xx response, and returns the JSON body on success.
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
body: dict[str, Any] = response.json()
|
|
25
|
+
except ValueError:
|
|
26
|
+
body = {}
|
|
27
|
+
|
|
28
|
+
if response.is_success:
|
|
29
|
+
return body
|
|
30
|
+
|
|
31
|
+
message = _extract_message(body) or response.reason_phrase or "request failed"
|
|
32
|
+
stage = body.get("error_stage") if isinstance(body, dict) else None
|
|
33
|
+
if response.status_code == 422 and stage:
|
|
34
|
+
raise StageError(
|
|
35
|
+
message=message,
|
|
36
|
+
status_code=response.status_code,
|
|
37
|
+
stage=str(stage),
|
|
38
|
+
response_body=body if isinstance(body, dict) else None,
|
|
39
|
+
)
|
|
40
|
+
raise APIError(
|
|
41
|
+
message=message,
|
|
42
|
+
status_code=response.status_code,
|
|
43
|
+
response_body=body if isinstance(body, dict) else None,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _extract_message(body: Any) -> Optional[str]:
|
|
48
|
+
if isinstance(body, dict):
|
|
49
|
+
for key in ("error", "message", "detail"):
|
|
50
|
+
value = body.get(key)
|
|
51
|
+
if isinstance(value, str) and value:
|
|
52
|
+
return value
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def wrap_transport_error(exc: httpx.HTTPError) -> VerdifaxConnectionError:
|
|
57
|
+
"""Translate an httpx transport error into a Verdifax ``ConnectionError``."""
|
|
58
|
+
return VerdifaxConnectionError(f"verdifax: failed to reach API: {exc}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = ["parse_response", "wrap_transport_error"]
|
verdifax/_validation.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Client-side validation helpers shared by the sync and async clients.
|
|
2
|
+
|
|
3
|
+
These mirror the validation the Verdifax API performs server-side (see
|
|
4
|
+
``cmd/api/main.go``). Performing the check client-side gives callers an
|
|
5
|
+
immediate :class:`~verdifax.exceptions.ValidationError` instead of an
|
|
6
|
+
HTTP 400 round-trip.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Union
|
|
12
|
+
|
|
13
|
+
from .exceptions import ValidationError
|
|
14
|
+
|
|
15
|
+
_HEX_CHARS = frozenset("0123456789abcdef")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_hex64(value: str, field_name: str) -> None:
|
|
19
|
+
"""Raise ``ValidationError`` unless ``value`` is 64 lowercase hex chars."""
|
|
20
|
+
if not isinstance(value, str):
|
|
21
|
+
raise ValidationError(f"{field_name} must be a string, got {type(value).__name__}")
|
|
22
|
+
if len(value) != 64:
|
|
23
|
+
raise ValidationError(
|
|
24
|
+
f"{field_name} must be 64-char hex, got {len(value)} chars"
|
|
25
|
+
)
|
|
26
|
+
lowered = value.lower()
|
|
27
|
+
if lowered != value:
|
|
28
|
+
raise ValidationError(f"{field_name} must be lowercase hex")
|
|
29
|
+
for c in lowered:
|
|
30
|
+
if c not in _HEX_CHARS:
|
|
31
|
+
raise ValidationError(f"{field_name} must be lowercase hex (offending char: {c!r})")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def validate_route_id(value: str) -> None:
|
|
35
|
+
"""Raise ``ValidationError`` unless ``value`` is a non-empty string."""
|
|
36
|
+
if not isinstance(value, str) or not value:
|
|
37
|
+
raise ValidationError("route_id must be a non-empty string")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def normalize_payload(payload: Union[str, bytes]) -> tuple[str, str]:
|
|
41
|
+
"""Return ``(payload_text, payload_b64)`` exactly one of which is non-empty.
|
|
42
|
+
|
|
43
|
+
Strings are sent as ``payload_text``. Bytes are decoded as UTF-8 if
|
|
44
|
+
possible so the API can hash them deterministically; otherwise they are
|
|
45
|
+
base64-encoded into the ``payload`` field.
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(payload, str):
|
|
48
|
+
return payload, ""
|
|
49
|
+
if isinstance(payload, bytes):
|
|
50
|
+
try:
|
|
51
|
+
return payload.decode("utf-8"), ""
|
|
52
|
+
except UnicodeDecodeError:
|
|
53
|
+
import base64
|
|
54
|
+
|
|
55
|
+
return "", base64.b64encode(payload).decode("ascii")
|
|
56
|
+
raise ValidationError(
|
|
57
|
+
f"payload must be str or bytes, got {type(payload).__name__}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = ["normalize_payload", "validate_hex64", "validate_route_id"]
|
verdifax/_version.py
ADDED
verdifax/async_client.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Asynchronous Verdifax API client.
|
|
2
|
+
|
|
3
|
+
Mirrors :class:`verdifax.VerdifaxClient` but uses ``httpx.AsyncClient``.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
>>> import asyncio
|
|
7
|
+
>>> from verdifax import AsyncVerdifaxClient
|
|
8
|
+
>>>
|
|
9
|
+
>>> async def main():
|
|
10
|
+
... async with AsyncVerdifaxClient() as client:
|
|
11
|
+
... receipt = await client.attest(
|
|
12
|
+
... payload="hello verdifax",
|
|
13
|
+
... program_id="a" * 64,
|
|
14
|
+
... route_id="route-test",
|
|
15
|
+
... registry_record_hash="b" * 64,
|
|
16
|
+
... )
|
|
17
|
+
... return receipt.manifest_hash
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
from types import TracebackType
|
|
24
|
+
from typing import Any, Optional, Type, Union
|
|
25
|
+
|
|
26
|
+
import httpx
|
|
27
|
+
|
|
28
|
+
from ._transport import parse_response, wrap_transport_error
|
|
29
|
+
from ._validation import normalize_payload, validate_hex64, validate_route_id
|
|
30
|
+
from ._version import __version__
|
|
31
|
+
from .client import (
|
|
32
|
+
DEFAULT_BASE_URL,
|
|
33
|
+
DEFAULT_TIMEOUT,
|
|
34
|
+
ENV_API_KEY,
|
|
35
|
+
ENV_BASE_URL,
|
|
36
|
+
)
|
|
37
|
+
from .exceptions import APIError
|
|
38
|
+
from .models import (
|
|
39
|
+
AttestationReceipt,
|
|
40
|
+
AttestedContext,
|
|
41
|
+
ExecuteRequest,
|
|
42
|
+
ExecutionManifest,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AsyncVerdifaxClient:
|
|
47
|
+
"""Async client for the Verdifax pipeline REST API.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
base_url: Base URL of the Verdifax API. Falls back to
|
|
51
|
+
``$VERDIFAX_API_URL`` and finally to ``http://localhost:9090``.
|
|
52
|
+
api_key: Optional API key sent as the ``X-Verdifax-Key`` header.
|
|
53
|
+
Falls back to ``$VERDIFAX_API_KEY``.
|
|
54
|
+
timeout: Request timeout in seconds. Defaults to 30.
|
|
55
|
+
transport: Optional custom ``httpx.AsyncBaseTransport`` for tests.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
base_url: Optional[str] = None,
|
|
61
|
+
api_key: Optional[str] = None,
|
|
62
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
63
|
+
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
self.base_url = (base_url or os.environ.get(ENV_BASE_URL) or DEFAULT_BASE_URL).rstrip("/")
|
|
66
|
+
self.api_key = api_key or os.environ.get(ENV_API_KEY)
|
|
67
|
+
self.timeout = timeout
|
|
68
|
+
|
|
69
|
+
headers = {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"Accept": "application/json",
|
|
72
|
+
"User-Agent": f"verdifax-python/{__version__} (async)",
|
|
73
|
+
}
|
|
74
|
+
if self.api_key:
|
|
75
|
+
headers["X-Verdifax-Key"] = self.api_key
|
|
76
|
+
|
|
77
|
+
self._http = httpx.AsyncClient(
|
|
78
|
+
base_url=self.base_url,
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
headers=headers,
|
|
81
|
+
transport=transport,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# ── lifecycle ──────────────────────────────────────────────────────────
|
|
85
|
+
async def close(self) -> None:
|
|
86
|
+
"""Release the underlying HTTP connection pool."""
|
|
87
|
+
await self._http.aclose()
|
|
88
|
+
|
|
89
|
+
async def __aenter__(self) -> "AsyncVerdifaxClient":
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
async def __aexit__(
|
|
93
|
+
self,
|
|
94
|
+
exc_type: Optional[Type[BaseException]],
|
|
95
|
+
exc_val: Optional[BaseException],
|
|
96
|
+
exc_tb: Optional[TracebackType],
|
|
97
|
+
) -> None:
|
|
98
|
+
await self.close()
|
|
99
|
+
|
|
100
|
+
# ── public API ─────────────────────────────────────────────────────────
|
|
101
|
+
async def health(self) -> dict[str, Any]:
|
|
102
|
+
"""Liveness check. Returns the JSON body of ``GET /health``."""
|
|
103
|
+
try:
|
|
104
|
+
response = await self._http.get("/health")
|
|
105
|
+
except httpx.HTTPError as exc:
|
|
106
|
+
raise wrap_transport_error(exc) from exc
|
|
107
|
+
return parse_response(response)
|
|
108
|
+
|
|
109
|
+
async def attest(
|
|
110
|
+
self,
|
|
111
|
+
payload: Union[str, bytes],
|
|
112
|
+
program_id: str,
|
|
113
|
+
route_id: str,
|
|
114
|
+
registry_record_hash: str,
|
|
115
|
+
attested_context: Optional[AttestedContext] = None,
|
|
116
|
+
) -> AttestationReceipt:
|
|
117
|
+
"""Async version of :meth:`VerdifaxClient.attest`.
|
|
118
|
+
|
|
119
|
+
Accepts the same optional ``attested_context`` block —
|
|
120
|
+
see :class:`verdifax.models.AttestedContext`.
|
|
121
|
+
"""
|
|
122
|
+
request_body = self._build_execute_body(
|
|
123
|
+
payload=payload,
|
|
124
|
+
program_id=program_id,
|
|
125
|
+
route_id=route_id,
|
|
126
|
+
registry_record_hash=registry_record_hash,
|
|
127
|
+
attested_context=attested_context,
|
|
128
|
+
)
|
|
129
|
+
try:
|
|
130
|
+
response = await self._http.post("/execute", json=request_body)
|
|
131
|
+
except httpx.HTTPError as exc:
|
|
132
|
+
raise wrap_transport_error(exc) from exc
|
|
133
|
+
body = parse_response(response)
|
|
134
|
+
if "manifest" not in body:
|
|
135
|
+
raise APIError(
|
|
136
|
+
message="API response missing 'manifest'",
|
|
137
|
+
status_code=response.status_code,
|
|
138
|
+
response_body=body,
|
|
139
|
+
)
|
|
140
|
+
return AttestationReceipt.from_api_response(body)
|
|
141
|
+
|
|
142
|
+
async def verify(
|
|
143
|
+
self,
|
|
144
|
+
manifest_hash: str,
|
|
145
|
+
payload: Union[str, bytes],
|
|
146
|
+
program_id: str,
|
|
147
|
+
route_id: str,
|
|
148
|
+
registry_record_hash: str,
|
|
149
|
+
attested_context: Optional[AttestedContext] = None,
|
|
150
|
+
) -> bool:
|
|
151
|
+
"""Async version of :meth:`VerdifaxClient.verify`."""
|
|
152
|
+
validate_hex64(manifest_hash, "manifest_hash")
|
|
153
|
+
receipt = await self.attest(
|
|
154
|
+
payload=payload,
|
|
155
|
+
program_id=program_id,
|
|
156
|
+
route_id=route_id,
|
|
157
|
+
registry_record_hash=registry_record_hash,
|
|
158
|
+
attested_context=attested_context,
|
|
159
|
+
)
|
|
160
|
+
return receipt.manifest_hash == manifest_hash
|
|
161
|
+
|
|
162
|
+
async def execute(
|
|
163
|
+
self,
|
|
164
|
+
payload: Union[str, bytes],
|
|
165
|
+
program_id: str,
|
|
166
|
+
route_id: str,
|
|
167
|
+
registry_record_hash: str,
|
|
168
|
+
attested_context: Optional[AttestedContext] = None,
|
|
169
|
+
) -> ExecutionManifest:
|
|
170
|
+
"""Async lower-level alternative returning just the manifest."""
|
|
171
|
+
receipt = await self.attest(
|
|
172
|
+
payload=payload,
|
|
173
|
+
program_id=program_id,
|
|
174
|
+
route_id=route_id,
|
|
175
|
+
registry_record_hash=registry_record_hash,
|
|
176
|
+
attested_context=attested_context,
|
|
177
|
+
)
|
|
178
|
+
return receipt.manifest
|
|
179
|
+
|
|
180
|
+
# ── internal helpers ───────────────────────────────────────────────────
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _build_execute_body(
|
|
183
|
+
payload: Union[str, bytes],
|
|
184
|
+
program_id: str,
|
|
185
|
+
route_id: str,
|
|
186
|
+
registry_record_hash: str,
|
|
187
|
+
attested_context: Optional[AttestedContext] = None,
|
|
188
|
+
) -> dict[str, Any]:
|
|
189
|
+
validate_hex64(program_id, "program_id")
|
|
190
|
+
validate_hex64(registry_record_hash, "registry_record_hash")
|
|
191
|
+
validate_route_id(route_id)
|
|
192
|
+
payload_text, payload_b64 = normalize_payload(payload)
|
|
193
|
+
# Auto-derive `attested = True` if any field carries a value, so
|
|
194
|
+
# callers don't have to remember to set the boolean explicitly.
|
|
195
|
+
if attested_context is not None:
|
|
196
|
+
attested_context = attested_context.with_auto_attested()
|
|
197
|
+
request = ExecuteRequest(
|
|
198
|
+
payload_text=payload_text or None,
|
|
199
|
+
payload=payload_b64 or None,
|
|
200
|
+
program_id=program_id,
|
|
201
|
+
route_id=route_id,
|
|
202
|
+
registry_record_hash=registry_record_hash,
|
|
203
|
+
attested_context=attested_context,
|
|
204
|
+
)
|
|
205
|
+
return request.model_dump_request()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
__all__ = ["AsyncVerdifaxClient"]
|