From cc70a86a7a4897ec0737cd641b5b3575fbdefc23 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Wed, 26 Aug 2026 14:29:45 -0400 Subject: [PATCH 1/9] Add strict hosted runner execution adapter --- openadapt_flow/runner/__init__.py | 47 +- openadapt_flow/runner/commands.py | 8 +- openadapt_flow/runner/config.py | 262 +++- openadapt_flow/runner/hosted_adapter.py | 1356 +++++++++++++++++++ openadapt_flow/runner/inputs.py | 95 ++ openadapt_flow/runner/product_release.py | 267 ++++ openadapt_flow/runner/protocol.py | 14 +- openadapt_flow/runner/verify.py | 60 +- openadapt_flow/runtime/durable/authority.py | 15 +- tests/test_durable_authority_v13.py | 47 + tests/test_hosted_runner_adapter.py | 686 ++++++++++ tests/test_runner_client_lib.py | 2 +- 12 files changed, 2809 insertions(+), 50 deletions(-) create mode 100644 openadapt_flow/runner/hosted_adapter.py create mode 100644 openadapt_flow/runner/inputs.py create mode 100644 openadapt_flow/runner/product_release.py create mode 100644 tests/test_hosted_runner_adapter.py diff --git a/openadapt_flow/runner/__init__.py b/openadapt_flow/runner/__init__.py index 013f6222..dd641fd5 100644 --- a/openadapt_flow/runner/__init__.py +++ b/openadapt_flow/runner/__init__.py @@ -1,15 +1,8 @@ -"""EXPERIMENTAL runner-client LIBRARY (verification, lease logic, evidence, -command mapping) for the hosted control-plane / local-execution-plane runner -protocol. NO daemon, NO network loop, NO CLI verb — deliberately. +"""Flow-owned hosted-runner verification and execution library. -Scope: the merged ``/api/runners/*`` control-plane surface in openadapt-cloud -(``src/lib/runners.ts``) is mock-gated (410 in live) and its transport-facing -half is scheduled to CHANGE before any customer daemon can ship (poll cadence -and hosting economics, lease renewal + sleep reclaim, control-verb channel, -mandatory params-by-reference for regulated orgs — see -``docs/design/RUNNER_CLIENT_LIBRARY.md`` for the verified findings and the -required revisions). This package therefore contains only the transport- -agnostic half that SURVIVES that revision: +Desktop owns the authenticated register, poll, and callback transport loop. +This package owns the transport-independent trust, admission, one-use, +governed-execution, and terminal-classification boundary: * :mod:`~openadapt_flow.runner.protocol` — strict typed models of the dispatch wire contract (contract drift is a refusal, not a best guess); @@ -31,7 +24,9 @@ evidence queue (a run that finishes offline reports late, never never); * :mod:`~openadapt_flow.runner.commands` — mapping of governed dispatch verbs onto the EXISTING CLI entry points (``run`` / ``resume``); unmappable verbs - refuse. + refuse; +* :mod:`~openadapt_flow.runner.hosted_adapter` — the strict Cloud lease wire, + protected local trust, managed child bridge, and no-replay result contract. """ from openadapt_flow.runner.commands import ( @@ -52,6 +47,21 @@ read_managed_dispatch_envelope, write_managed_dispatch_envelope, ) +from openadapt_flow.runner.hosted_adapter import ( + CallbackRequest, + CallbackResponse, + DeliveryAuthority, + HostedDispatch, + HostedDispatchRefusal, + HostedRecoveryBinding, + HostedRunnerAdapter, + HostedRunnerTransport, + HostedRunResult, + PollRequest, + RegisterCapabilities, + RegisterRequest, + RegisterResponse, +) from openadapt_flow.runner.lease import ( CompletionDisposition, LeaseError, @@ -78,8 +88,17 @@ __all__ = [ "CompletionDisposition", + "CallbackRequest", + "CallbackResponse", + "DeliveryAuthority", "DispatchParseError", "EvidenceOutbox", + "HostedDispatch", + "HostedDispatchRefusal", + "HostedRecoveryBinding", + "HostedRunResult", + "HostedRunnerAdapter", + "HostedRunnerTransport", "LeaseError", "ManagedDispatchEnvelope", "ManagedDispatchEnvelopeError", @@ -88,6 +107,10 @@ "LeasedDispatch", "Refusal", "RefusalCode", + "PollRequest", + "RegisterCapabilities", + "RegisterRequest", + "RegisterResponse", "RunnerConfig", "RunnerConfigError", "RunnerDispatchPayload", diff --git a/openadapt_flow/runner/commands.py b/openadapt_flow/runner/commands.py index a92d2a32..6feee772 100644 --- a/openadapt_flow/runner/commands.py +++ b/openadapt_flow/runner/commands.py @@ -3,9 +3,8 @@ The runner never grows a private execution path: a dispatched run is the same fail-closed ``openadapt-flow run`` admission gate + shared replayer the local CLI uses, in a child process (crash isolation; the design doc's "the agent -shells them"). This module only BUILDS argv — executing it belongs to the -future daemon, which is deliberately not in this library (see -``docs/design/RUNNER_CLIENT_LIBRARY.md``). +shells them"). This module only builds argv. The hosted adapter executes that +argv in the managed child process. Verb coverage, honestly stated: @@ -51,6 +50,7 @@ def build_run_argv( params_file: Optional[Path], *, managed_dispatch_file: Path, + qualification_authority_file: Optional[Path] = None, ) -> list[str]: """The exact governed CLI invocation for a verified ``run`` dispatch. @@ -81,6 +81,8 @@ def build_run_argv( ] if params_file is not None: argv += ["--params-file", str(params_file)] + if qualification_authority_file is not None: + argv += ["--qualification-authority-file", str(qualification_authority_file)] if verified.bundle.policy: argv += ["--policy", verified.bundle.policy] if ( diff --git a/openadapt_flow/runner/config.py b/openadapt_flow/runner/config.py index 30b72e48..d752542c 100644 --- a/openadapt_flow/runner/config.py +++ b/openadapt_flow/runner/config.py @@ -3,7 +3,7 @@ Nothing writes this file programmatically. It names the deployment profiles a dispatch may reference and the exact sealed bundles (by content digest) this machine is willing to execute — a digest absent from this file is refused. -That is the no-remote-code-delivery hard line: the future runner daemon only +That is the no-remote-code-delivery hard line: the hosted adapter only ever executes bundles the operator ALREADY installed and listed here; the dispatch's ``bundle.url`` is never fetched. @@ -42,16 +42,25 @@ import os import re +import stat from dataclasses import dataclass, field from pathlib import Path from typing import Any, Optional from openadapt_flow.hosted import HostedError +from openadapt_flow.private_file import ( + PrivateFileAclError, + windows_descriptor_has_private_acl, +) _HEX64_RE = re.compile(r"^[a-f0-9]{64}$") +_UUID_RE = re.compile( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" +) +_SAFE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,199}$") -def _load_manifest_toml(path: Path) -> dict[str, Any]: +def _load_manifest_toml(path: Path, *, protected: bool = False) -> dict[str, Any]: """Full-TOML parse (the manifest uses ``[[bundles]]`` array tables, which ``hosted._load_toml``'s 3.10 minimal fallback cannot represent). Uses stdlib ``tomllib`` on 3.11+ and the declared ``tomli`` dependency on 3.10. @@ -60,6 +69,81 @@ def _load_manifest_toml(path: Path) -> dict[str, Any]: import tomllib except ModuleNotFoundError: # pragma: no cover - Python 3.10 import tomli as tomllib # type: ignore[no-redef] + if protected: + flags = os.O_RDONLY + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + try: + path_before = path.lstat() + if not stat.S_ISREG(path_before.st_mode) or stat.S_ISLNK( + path_before.st_mode + ): + raise RunnerConfigError( + "hosted runner manifest is not a private regular file" + ) + descriptor = os.open(path, flags) + except OSError as exc: + raise RunnerConfigError( + "hosted runner manifest could not be opened safely" + ) from exc + try: + before = os.fstat(descriptor) + try: + private = ( + windows_descriptor_has_private_acl(descriptor) + if os.name == "nt" + else ( + before.st_uid == os.geteuid() + and stat.S_IMODE(before.st_mode) == 0o600 + ) + ) + except PrivateFileAclError as exc: + raise RunnerConfigError( + "hosted runner manifest ACL could not be verified" + ) from exc + if ( + not stat.S_ISREG(before.st_mode) + or before.st_size > 1024 * 1024 + or not private + ): + raise RunnerConfigError( + "hosted runner manifest is not a private regular file" + ) + chunks: list[bytes] = [] + remaining = before.st_size + while remaining: + chunk = os.read(descriptor, min(remaining, 64 * 1024)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + raw = b"".join(chunks) + after = os.fstat(descriptor) + try: + path_after = path.lstat() + except OSError as exc: + raise RunnerConfigError( + "hosted runner manifest changed during its protected read" + ) from exc + if ( + len(raw) != before.st_size + or stat.S_ISLNK(path_after.st_mode) + or (path_before.st_dev, path_before.st_ino) + != (before.st_dev, before.st_ino) + or (path_after.st_dev, path_after.st_ino) + != (before.st_dev, before.st_ino) + or (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) + != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns) + ): + raise RunnerConfigError( + "hosted runner manifest changed during its protected read" + ) + finally: + os.close(descriptor) + try: + return tomllib.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, tomllib.TOMLDecodeError) as exc: + raise RunnerConfigError("hosted runner manifest is not valid TOML") from exc try: with path.open("rb") as fh: return tomllib.load(fh) @@ -103,6 +187,9 @@ class TrustedBundle: allow_unverified_writes: bool = False #: Local escape hatch mirroring ``run --allow-unencrypted``. allow_unencrypted: bool = False + #: Exact archive/object digest named by the workflow admission. Hosted + #: execution requires this local pin; ordinary local execution does not. + artifact_sha256: Optional[str] = None @dataclass(frozen=True) @@ -112,6 +199,32 @@ class BusinessDecisionServiceConfig: key_file: Path +@dataclass(frozen=True) +class LocalRuntimeRelease: + """One independently installed target release used during enrollment.""" + + target: str + admission_id: str + admission_sha256: str + release_version: str + release_artifact_sha256: str + + +@dataclass(frozen=True) +class AdmissionTrustFiles: + """Local signer and revocation state used to verify hosted admissions.""" + + signer_registry: Path + state: Path + + +@dataclass(frozen=True) +class WorkflowAdmissionTrustFiles(AdmissionTrustFiles): + """Local v2 expectation that is independent from the leased artifact.""" + + expected_bindings: Path + + @dataclass(frozen=True) class RunnerConfig: """Parsed trust manifest.""" @@ -121,9 +234,14 @@ class RunnerConfig: profiles: dict[str, Path] = field(default_factory=dict) bundles: dict[str, TrustedBundle] = field(default_factory=dict) #: Capability advertisement (deployment.yaml backend kinds this machine - #: can drive) for the future register/poll payloads. Advisory only. + #: can drive) for hosted registration. Advisory only. backends: tuple[str, ...] = ("web",) business_decisions: Optional[BusinessDecisionServiceConfig] = None + local_runtime_release: tuple[LocalRuntimeRelease, ...] = () + product_release_admission: Optional[AdmissionTrustFiles] = None + workflow_admission: Optional[WorkflowAdmissionTrustFiles] = None + params_ref_root: Optional[Path] = None + evidence_runner_private_key: Optional[Path] = None def _parse_param_patterns(raw: object, index: int) -> dict[str, str]: @@ -147,7 +265,9 @@ def _parse_param_patterns(raw: object, index: int) -> dict[str, str]: return patterns -def load_runner_config(path: Optional[Path] = None) -> RunnerConfig: +def load_runner_config( + path: Optional[Path] = None, *, protected: bool = False +) -> RunnerConfig: """Load and validate ``runner.toml``. Fail loudly on anything malformed.""" cfg_path = path or runner_config_path() if not cfg_path.is_file(): @@ -156,7 +276,7 @@ def load_runner_config(path: Optional[Path] = None) -> RunnerConfig: "list the deployment profiles and the exact sealed bundles (by " "content digest) this machine may execute." ) - data = _load_manifest_toml(cfg_path) + data = _load_manifest_toml(cfg_path, protected=protected) runner_tbl = data.get("runner") or {} if not isinstance(runner_tbl, dict): @@ -214,7 +334,18 @@ def load_runner_config(path: Optional[Path] = None) -> RunnerConfig: param_patterns=_parse_param_patterns(entry.get("param_patterns"), i), allow_unverified_writes=bool(entry.get("allow_unverified_writes", False)), allow_unencrypted=bool(entry.get("allow_unencrypted", False)), + artifact_sha256=( + str(entry["artifact_sha256"]) + if entry.get("artifact_sha256") is not None + else None + ), ) + if bundles[digest].artifact_sha256 is not None and not _HEX64_RE.fullmatch( + bundles[digest].artifact_sha256 or "" + ): + raise RunnerConfigError( + f"[[bundles]] entry {i} artifact_sha256 must be 64 lowercase hex" + ) decision_tbl = data.get("business_decisions") business_decisions = None @@ -237,6 +368,122 @@ def load_runner_config(path: Optional[Path] = None) -> RunnerConfig: ) business_decisions = BusinessDecisionServiceConfig(key_file=key_file) + local_release_tbl = data.get("local_runtime_release") or {} + if not isinstance(local_release_tbl, dict): + raise RunnerConfigError("[local_runtime_release] must be a table") + local_runtime_release: list[LocalRuntimeRelease] = [] + expected_release_targets = ("flow", "desktop", "capture") + for target in expected_release_targets: + entry = local_release_tbl.get(target) + if entry is None: + continue + if not isinstance(entry, dict) or set(entry) != { + "admission_id", + "admission_sha256", + "release_version", + "release_artifact_sha256", + }: + raise RunnerConfigError( + f"[local_runtime_release.{target}] has an invalid exact shape" + ) + admission_sha256 = str(entry["admission_sha256"]) + artifact_sha256 = str(entry["release_artifact_sha256"]) + admission_id = str(entry["admission_id"]) + release_version = str(entry["release_version"]) + if not _HEX64_RE.fullmatch(admission_sha256) or not _HEX64_RE.fullmatch( + artifact_sha256 + ): + raise RunnerConfigError( + f"[local_runtime_release.{target}] contains an invalid digest" + ) + if not _UUID_RE.fullmatch(admission_id) or not _SAFE_ID_RE.fullmatch( + release_version + ): + raise RunnerConfigError( + f"[local_runtime_release.{target}] contains an invalid identity" + ) + local_runtime_release.append( + LocalRuntimeRelease( + target=target, + admission_id=admission_id, + admission_sha256=admission_sha256, + release_version=release_version, + release_artifact_sha256=artifact_sha256, + ) + ) + unknown_release_targets = sorted( + set(local_release_tbl).difference(expected_release_targets) + ) + if unknown_release_targets: + raise RunnerConfigError( + "[local_runtime_release] contains unknown target(s): " + + ", ".join(unknown_release_targets) + ) + + def admission_trust_files(table_name: str) -> Optional[AdmissionTrustFiles]: + table = data.get(table_name) + if table is None: + return None + if not isinstance(table, dict) or set(table) != {"signer_registry", "state"}: + raise RunnerConfigError(f"[{table_name}] has an invalid exact shape") + registry = Path(str(table["signer_registry"])).expanduser() + state = Path(str(table["state"])).expanduser() + if not registry.is_file() or not state.is_file(): + raise RunnerConfigError( + f"[{table_name}] trust files must be existing regular files" + ) + return AdmissionTrustFiles(signer_registry=registry, state=state) + + product_release_admission = admission_trust_files("product_release_admission") + workflow_table = data.get("workflow_admission") + workflow_admission = None + if workflow_table is not None: + if not isinstance(workflow_table, dict) or set(workflow_table) != { + "signer_registry", + "state", + "expected_bindings", + }: + raise RunnerConfigError("[workflow_admission] has an invalid exact shape") + workflow_paths = { + key: Path(str(workflow_table[key])).expanduser() + for key in ("signer_registry", "state", "expected_bindings") + } + if any(not path.is_file() for path in workflow_paths.values()): + raise RunnerConfigError( + "[workflow_admission] trust files must be existing regular files" + ) + workflow_admission = WorkflowAdmissionTrustFiles( + signer_registry=workflow_paths["signer_registry"], + state=workflow_paths["state"], + expected_bindings=workflow_paths["expected_bindings"], + ) + + params_tbl = data.get("params") + params_ref_root = None + if params_tbl is not None: + if not isinstance(params_tbl, dict) or set(params_tbl) != {"protected_root"}: + raise RunnerConfigError("[params] has an invalid exact shape") + params_ref_root = Path(str(params_tbl["protected_root"])).expanduser() + if not params_ref_root.is_dir(): + raise RunnerConfigError( + "params.protected_root must be an existing directory" + ) + + evidence_tbl = data.get("evidence_runner") + evidence_runner_private_key = None + if evidence_tbl is not None: + if not isinstance(evidence_tbl, dict) or set(evidence_tbl) != { + "private_key_file" + }: + raise RunnerConfigError("[evidence_runner] has an invalid exact shape") + evidence_runner_private_key = Path( + str(evidence_tbl["private_key_file"]) + ).expanduser() + if not evidence_runner_private_key.is_file(): + raise RunnerConfigError( + "evidence_runner.private_key_file must be an existing file" + ) + return RunnerConfig( name=name, host=host, @@ -244,4 +491,9 @@ def load_runner_config(path: Optional[Path] = None) -> RunnerConfig: bundles=bundles, backends=tuple(str(b).strip() for b in backends_raw), business_decisions=business_decisions, + local_runtime_release=tuple(local_runtime_release), + product_release_admission=product_release_admission, + workflow_admission=workflow_admission, + params_ref_root=params_ref_root, + evidence_runner_private_key=evidence_runner_private_key, ) diff --git a/openadapt_flow/runner/hosted_adapter.py b/openadapt_flow/runner/hosted_adapter.py new file mode 100644 index 00000000..45c16fb7 --- /dev/null +++ b/openadapt_flow/runner/hosted_adapter.py @@ -0,0 +1,1356 @@ +"""Strict Flow-owned bridge between a hosted lease and governed execution. + +The Desktop host owns HTTP and credential storage. This module owns every +decision that can authorize or classify execution: admission verification, +local trust, input resolution, one-use reservation, managed child execution, +evidence projection, and terminal verification. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import stat +import subprocess +from base64 import b64decode, b64encode +from dataclasses import dataclass, field, replace +from datetime import datetime, timezone +from pathlib import Path, PurePosixPath +from typing import Any, Callable, Literal, Mapping, Protocol, Union +from urllib.parse import urlsplit + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from openadapt_flow.ir import RunReport, Workflow +from openadapt_flow.private_file import ( + PrivateFileAclError, + windows_descriptor_has_private_acl, +) +from openadapt_flow.production_qualification import ( + ProductionQualificationAuthority, + ProductionQualificationGuard, + _read_private_json, +) +from openadapt_flow.qualification_admission_v2 import ( + QualificationAdmissionEnvelope, + QualificationAdmissionExpected, + QualificationSignerRegistry, + contract_sha256, + verify_qualification_admission, +) +from openadapt_flow.runner.commands import build_run_argv +from openadapt_flow.runner.config import RunnerConfig, load_runner_config +from openadapt_flow.runner.dispatch_envelope import write_managed_dispatch_envelope +from openadapt_flow.runner.evidence import failure_events, refusal_events, report_events +from openadapt_flow.runner.inputs import resolve_admitted_params +from openadapt_flow.runner.product_release import ( + ProductReleaseAdmissionArtifact, + ProductReleaseAdmissionPayload, + load_product_release_signer_trust, + verify_product_release_admission, +) +from openadapt_flow.runner.protocol import DispatchParamsValues, RunnerDispatchPayload +from openadapt_flow.runner.verify import Refusal, RefusalCode, verify_dispatch +from openadapt_flow.runtime.durable.authority import ( + REMOTE_AUTHORITY_TOKEN_ENV, + REMOTE_AUTHORITY_URL_ENV, + REMOTE_DISPATCH_SESSION_ID_ENV, +) +from openadapt_flow.terminal_verification_v2 import ( + ProductionTerminalVerificationEnvelope, + evidence_runner_signer_sha256, +) +from openadapt_flow.transaction import ( + DuplicateActuation, + IdempotencyLedger, + TransactionOutcome, + classify_transaction_outcome, +) + +_UUID = r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" +_HEX64 = r"^[a-f0-9]{64}$" +_IDEMPOTENCY = r"^[A-Za-z0-9][A-Za-z0-9._:-]{15,199}$" +_LEASE_TOKEN = r"^oal_[a-f0-9]{64}$" +_RUNNER_TOKEN = r"^oar_[a-f0-9]{64}$" +_SAFE_ID = r"^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,199}$" +_UTC_SECONDS = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$") +_MAX_ARTIFACT_BYTES = 2 * 1024 * 1024 + + +def _utc_seconds(value: str, *, label: str) -> datetime: + if _UTC_SECONDS.fullmatch(value) is None: + raise ValueError(f"{label} is not canonical UTC seconds") + try: + return datetime.fromisoformat(value[:-1] + "+00:00") + except ValueError as exc: + raise ValueError(f"{label} is not canonical UTC seconds") from exc + + +class _Closed(BaseModel): + model_config = ConfigDict(strict=True, extra="forbid", frozen=True) + + +class LocalRuntimeReleaseBinding(_Closed): + target: Literal["flow", "desktop", "capture"] + admission_id: str = Field(pattern=_UUID) + admission_sha256: str = Field(pattern=_HEX64) + release_version: str = Field(pattern=_SAFE_ID) + release_artifact_sha256: str = Field(pattern=_HEX64) + + +CapabilityKind = Literal[ + "web", "windows", "macos", "linux", "rdp", "citrix", "rdp_window" +] + + +class RegisterCapabilities(_Closed): + backends: tuple[CapabilityKind, ...] = Field(min_length=1, max_length=16) + attended: bool + effects_substrates: tuple[CapabilityKind, ...] = Field(min_length=1, max_length=16) + + @model_validator(mode="after") + def _closed_capabilities(self) -> "RegisterCapabilities": + for label, values in ( + ("backend", self.backends), + ("effect substrate", self.effects_substrates), + ): + if len(values) != len(set(values)): + raise ValueError(f"runner {label} capabilities are invalid") + return self + + +class RegisterRequest(_Closed): + schema_version: Literal["openadapt.hosted-runner-registration/v1"] = ( + "openadapt.hosted-runner-registration/v1" + ) + name: str = Field(min_length=1, max_length=80) + platform: Literal["windows", "macos", "linux"] + agent_version: str = Field(min_length=1, max_length=40) + engine_version: str = Field(min_length=1, max_length=40) + mode: Literal["attended", "service"] + capabilities: RegisterCapabilities + local_runtime_release: dict[ + Literal["flow", "desktop", "capture"], LocalRuntimeReleaseBinding + ] + + @model_validator(mode="after") + def _exact_local_targets(self) -> "RegisterRequest": + if set(self.local_runtime_release) != {"flow", "desktop", "capture"} or any( + key != item.target for key, item in self.local_runtime_release.items() + ): + raise ValueError( + "local runtime release targets must be flow, desktop, capture" + ) + return self + + +class RegisterResponse(_Closed): + schema_version: Literal["openadapt.hosted-runner-registration-result/v1"] + runner_id: str = Field(pattern=_UUID) + tenant_id: str = Field(pattern=_UUID) + runner_session_id: str = Field(pattern=_UUID) + runner_token: str = Field(pattern=_RUNNER_TOKEN, repr=False) + token_expires_at: str + + @model_validator(mode="after") + def _canonical_expiry(self) -> "RegisterResponse": + _utc_seconds(self.token_expires_at, label="runner token expiry") + return self + + +class PollRequest(_Closed): + schema_version: Literal["openadapt.hosted-runner-poll/v1"] = ( + "openadapt.hosted-runner-poll/v1" + ) + runner_session_id: str = Field(pattern=_UUID) + wait_seconds: int = Field(ge=0, le=25) + lease_seconds: int = Field(ge=1, le=900) + + +class AdmissionArtifactBytes(_Closed): + artifact_bytes_base64: str = Field(min_length=4, max_length=2_796_204) + artifact_sha256: str = Field(pattern=_HEX64) + + def decode(self) -> bytes: + try: + raw = b64decode(self.artifact_bytes_base64, validate=True) + except ValueError as exc: + raise ValueError("admission artifact is not canonical base64") from exc + if len(raw) > _MAX_ARTIFACT_BYTES: + raise ValueError("admission artifact exceeds the size limit") + if b64encode(raw).decode("ascii") != self.artifact_bytes_base64: + raise ValueError("admission artifact is not canonical base64") + if hashlib.sha256(raw).hexdigest() != self.artifact_sha256: + raise ValueError("admission artifact digest does not match its bytes") + return raw + + @model_validator(mode="after") + def _bytes_match_digest(self) -> "AdmissionArtifactBytes": + self.decode() + return self + + +class HostedDispatch(_Closed): + schema_version: Literal["openadapt.hosted-runner/v1"] + dispatch_id: str = Field(pattern=_UUID) + tenant_id: str = Field(pattern=_UUID) + runner_id: str = Field(pattern=_UUID) + runner_session_id: str = Field(pattern=_UUID) + dispatch_session_id: str = Field(pattern=_UUID) + run_id: str = Field(pattern=_UUID) + workflow_id: str = Field(pattern=_UUID) + workflow_version_id: str = Field(pattern=_UUID) + idempotency_key: str = Field(pattern=_IDEMPOTENCY) + lease_token: str = Field(pattern=_LEASE_TOKEN, repr=False) + lease_expires_at: str + product_release_admission: AdmissionArtifactBytes + workflow_admission: AdmissionArtifactBytes + managed_delivery_authority_url: str = Field(min_length=1, max_length=2048) + delivery_authority_token: str = Field(pattern=_HEX64, repr=False) + payload: RunnerDispatchPayload + + @model_validator(mode="after") + def _exact_run_binding(self) -> "HostedDispatch": + _utc_seconds(self.lease_expires_at, label="hosted lease expiry") + if ( + self.payload.run_id != self.run_id + or self.payload.workflow_id != self.workflow_id + ): + raise ValueError("hosted lease identity does not match its payload") + if self.payload.bundle.version_id != self.workflow_version_id: + raise ValueError("hosted lease workflow version does not match its bundle") + return self + + +class HostedRecoveryBinding(_Closed): + """Callback state without params or the delivery-authority credential. + + This projection remains credential-bearing because it retains the lease + token required for the exact terminal callback. + """ + + schema_version: Literal["openadapt.hosted-runner-recovery/v1"] = ( + "openadapt.hosted-runner-recovery/v1" + ) + dispatch_id: str = Field(pattern=_UUID) + runner_session_id: str = Field(pattern=_UUID) + dispatch_session_id: str = Field(pattern=_UUID) + run_id: str = Field(pattern=_UUID) + workflow_id: str = Field(pattern=_UUID) + idempotency_key: str = Field(pattern=_IDEMPOTENCY) + lease_token: str = Field(pattern=_LEASE_TOKEN, repr=False) + product_release_admission_sha256: str = Field(pattern=_HEX64) + workflow_admission_sha256: str = Field(pattern=_HEX64) + bundle_content_digest: str = Field(pattern=_HEX64) + authorization_id: str = Field(min_length=1, max_length=128) + + +class HostedTerminalEvent(_Closed): + schema_version: Literal["openadapt.hosted-runner-terminal/v1"] = ( + "openadapt.hosted-runner-terminal/v1" + ) + run_id: str = Field(pattern=_UUID) + outcome: Literal[ + "VERIFIED", + "HALTED_BEFORE_EFFECT", + "RECONCILIATION_REQUIRED", + "FAILED_PLATFORM", + "CANCELED", + "REJECTED_POLICY", + "COMPLETED_UNVERIFIED", + "ROLLED_BACK", + ] + report_sha256: str = Field(pattern=_HEX64) + started: bool + uncertain_delivery: bool + terminal_verification_artifact_bytes_base64: str | None = Field( + default=None, max_length=2_796_204 + ) + terminal_verification_artifact_sha256: str | None = Field( + default=None, pattern=_HEX64 + ) + + @model_validator(mode="after") + def _verified_requires_exact_proof(self) -> "HostedTerminalEvent": + has_proof = self.terminal_verification_artifact_bytes_base64 is not None + if has_proof != (self.terminal_verification_artifact_sha256 is not None): + raise ValueError("terminal verification binding is incomplete") + if self.outcome == "VERIFIED" and not has_proof: + raise ValueError("VERIFIED requires exact terminal verification") + if self.outcome != "VERIFIED" and has_proof: + raise ValueError("non-VERIFIED callback cannot carry a success proof") + return self + + +class HostedRunResult(_Closed): + kind: Literal["result"] = "result" + dispatch_id: str = Field(pattern=_UUID) + run_id: str = Field(pattern=_UUID) + outcome: TransactionOutcome + evidence_batch: tuple[dict[str, Any], ...] + terminal_verification: ProductionTerminalVerificationEnvelope | None = None + started: bool + uncertain_delivery: bool + report_sha256: str = Field(pattern=_HEX64) + + @model_validator(mode="after") + def _closed_terminal(self) -> "HostedRunResult": + if (self.outcome is TransactionOutcome.VERIFIED) != ( + self.terminal_verification is not None + ): + raise ValueError("only a terminally verified result can be VERIFIED") + if self.uncertain_delivery and self.outcome not in { + TransactionOutcome.RECONCILIATION_REQUIRED, + TransactionOutcome.VERIFIED, + }: + raise ValueError("uncertain delivery has an invalid terminal outcome") + return self + + +class HostedDispatchRefusal(_Closed): + kind: Literal["refusal"] = "refusal" + dispatch_id: str | None = None + run_id: str | None = None + code: str = Field(min_length=1, max_length=64) + detail: str = Field(min_length=1, max_length=400) + evidence_batch: tuple[dict[str, Any], ...] = () + started: Literal[False] = False + uncertain_delivery: Literal[False] = False + outcome: Literal["REJECTED_POLICY"] = "REJECTED_POLICY" + report_sha256: str = Field(default="0" * 64, pattern=_HEX64) + + +class CallbackRequest(_Closed): + schema_version: Literal["openadapt.hosted-runner-callback/v1"] = ( + "openadapt.hosted-runner-callback/v1" + ) + dispatch_id: str = Field(pattern=_UUID) + runner_session_id: str = Field(pattern=_UUID) + idempotency_key: str = Field(pattern=_IDEMPOTENCY) + lease_token: str = Field(pattern=_LEASE_TOKEN, repr=False) + product_release_admission_sha256: str = Field(pattern=_HEX64) + workflow_admission_sha256: str = Field(pattern=_HEX64) + events: tuple[dict[str, Any], ...] = Field(min_length=1, max_length=10_001) + + +class CallbackResponse(_Closed): + schema_version: Literal["openadapt.hosted-runner-callback-result/v1"] + status: Literal["accepted", "duplicate"] + run_id: str = Field(pattern=_UUID) + outcome: TransactionOutcome + dispatch_state: Literal["closed"] + accepted_events: int = Field(ge=0, le=10_001) + + +class HostedRunnerTransport(Protocol): + """Desktop-owned HTTP surface. Credentials stay in its transport state.""" + + def register(self, request: RegisterRequest) -> RegisterResponse: ... + + def poll(self, request: PollRequest) -> HostedDispatch | None: ... + + def callback(self, run_id: str, request: CallbackRequest) -> CallbackResponse: ... + + +@dataclass(frozen=True) +class DeliveryAuthority: + """Run-scoped configuration for the existing per-input-edge authority path.""" + + url: str + token: str = field(repr=False) + + def __post_init__(self) -> None: + try: + parsed = urlsplit(self.url) + except ValueError as exc: + raise ValueError("managed delivery authority URL is invalid") from exc + if ( + parsed.scheme != "https" + or not parsed.hostname + or parsed.username is not None + or parsed.password is not None + or parsed.query + or parsed.fragment + or parsed.path != "/api/internal/managed-delivery-permit" + ): + raise ValueError( + "managed delivery authority URL is not a pinned HTTPS edge" + ) + if re.fullmatch(_HEX64, self.token) is None: + raise ValueError("managed delivery authority token is invalid") + + def child_environment(self) -> dict[str, str]: + return { + REMOTE_AUTHORITY_URL_ENV: self.url, + REMOTE_AUTHORITY_TOKEN_ENV: self.token, + } + + +@dataclass(frozen=True) +class ManagedExecution: + returncode: int + report_bytes: bytes | None + terminal_verification: ProductionTerminalVerificationEnvelope | None = None + + +ManagedRunner = Callable[[list[str], Path, Mapping[str, str]], ManagedExecution] + + +def _subprocess_runner( + argv: list[str], run_dir: Path, child_env: Mapping[str, str] +) -> ManagedExecution: + process = subprocess.run( # nosec - argv is built from verified local material + argv, + capture_output=True, + text=True, + env=dict(child_env), + ) + report_path = run_dir / "report.json" + report_bytes = report_path.read_bytes() if report_path.is_file() else None + proof_path = run_dir / "production-terminal-verification.json" + proof = None + if proof_path.is_file(): + proof = ProductionTerminalVerificationEnvelope.model_validate_json( + proof_path.read_bytes() + ) + return ManagedExecution(process.returncode, report_bytes, proof) + + +class HostedRunnerAdapter: + def __init__( + self, + ledger_path: Path, + *, + runner: ManagedRunner = _subprocess_runner, + ) -> None: + self.ledger_path = Path(ledger_path) + self._ledger = IdempotencyLedger( + self.ledger_path, namespace="openadapt-hosted-runner/v1" + ) + self._runner = runner + self._release_state_path = self.ledger_path.with_suffix( + self.ledger_path.suffix + ".product-release.json" + ) + + @staticmethod + def _protected_runner_origin(config: RunnerConfig) -> str: + raw = config.host + if raw is None: + raise ValueError("hosted runner requires a protected runner host origin") + try: + parsed = urlsplit(raw) + port = parsed.port + except ValueError as exc: + raise ValueError("protected runner host origin is invalid") from exc + canonical = f"https://{parsed.netloc}" + if ( + parsed.scheme != "https" + or not parsed.hostname + or parsed.hostname != parsed.hostname.lower() + or parsed.netloc != parsed.netloc.lower() + or parsed.username is not None + or parsed.password is not None + or parsed.path + or parsed.query + or parsed.fragment + or port == 443 + or raw != canonical + ): + raise ValueError("protected runner host is not one canonical HTTPS origin") + return canonical + + def registration_request( + self, + *, + runner_config: Path, + name: str, + platform: str, + agent_version: str, + engine_version: str, + mode: str, + capabilities: RegisterCapabilities | Mapping[str, object], + ) -> RegisterRequest: + config = load_runner_config(runner_config, protected=True) + self._protected_runner_origin(config) + releases = config.local_runtime_release + if tuple(item.target for item in releases) != ("flow", "desktop", "capture"): + raise ValueError( + "hosted registration requires exact flow, desktop, and capture releases" + ) + if not isinstance(capabilities, RegisterCapabilities): + if not isinstance(capabilities, Mapping) or set(capabilities) != { + "backends", + "attended", + "effects_substrates", + }: + raise ValueError("runner capabilities have an invalid exact shape") + backends = capabilities["backends"] + effects = capabilities["effects_substrates"] + attended = capabilities["attended"] + if ( + not isinstance(backends, (list, tuple)) + or not isinstance(effects, (list, tuple)) + or type(attended) is not bool + ): + raise ValueError("runner capabilities have an invalid exact shape") + capabilities = RegisterCapabilities( + backends=tuple(backends), + attended=attended, + effects_substrates=tuple(effects), + ) + return RegisterRequest( + name=name, + platform=platform, + agent_version=agent_version, + engine_version=engine_version, + mode=mode, + capabilities=capabilities, + local_runtime_release={ + item.target: LocalRuntimeReleaseBinding(**item.__dict__) + for item in releases + }, + ) + + @staticmethod + def _load_json(path: Path) -> object: + try: + return _read_private_json(path) + except (OSError, ValueError) as exc: + raise ValueError(f"admission trust state {path} is invalid") from exc + + @staticmethod + def _read_private_bytes(path: Path, *, maximum_bytes: int, label: str) -> bytes: + """Read one owner-only regular file without following a final link.""" + + path = Path(path) + flags = os.O_RDONLY + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + try: + path_before = path.lstat() + if not stat.S_ISREG(path_before.st_mode) or stat.S_ISLNK( + path_before.st_mode + ): + raise ValueError(f"{label} is not a private regular file") + descriptor = os.open(path, flags) + except OSError as exc: + raise ValueError(f"{label} could not be opened safely") from exc + try: + before = os.fstat(descriptor) + try: + private_permissions = ( + windows_descriptor_has_private_acl(descriptor) + if os.name == "nt" + else ( + before.st_uid == os.geteuid() + and stat.S_IMODE(before.st_mode) == 0o600 + ) + ) + except PrivateFileAclError as exc: + raise ValueError(f"{label} ACL could not be verified") from exc + if ( + not stat.S_ISREG(before.st_mode) + or before.st_size > maximum_bytes + or not private_permissions + ): + raise ValueError(f"{label} is not a private regular file") + chunks: list[bytes] = [] + remaining = min(before.st_size, maximum_bytes) + 1 + while remaining: + chunk = os.read(descriptor, min(remaining, 64 * 1024)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + raw = b"".join(chunks) + after = os.fstat(descriptor) + try: + path_after = path.lstat() + except OSError as exc: + raise ValueError(f"{label} changed during its protected read") from exc + if ( + len(raw) != before.st_size + or len(raw) > maximum_bytes + or stat.S_ISLNK(path_after.st_mode) + or (path_before.st_dev, path_before.st_ino) + != (before.st_dev, before.st_ino) + or (path_after.st_dev, path_after.st_ino) + != (before.st_dev, before.st_ino) + or (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) + != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns) + ): + raise ValueError(f"{label} changed during its protected read") + return raw + finally: + os.close(descriptor) + + def _load_evidence_private_key(self, config: RunnerConfig) -> Ed25519PrivateKey: + path = config.evidence_runner_private_key + if path is None: + raise ValueError("hosted runner has no evidence-runner private key") + raw = self._read_private_bytes( + path, maximum_bytes=4096, label="evidence-runner private key" + ) + try: + if len(raw) == 32: + key = Ed25519PrivateKey.from_private_bytes(raw) + else: + loaded = serialization.load_pem_private_key(raw, password=None) + if not isinstance(loaded, Ed25519PrivateKey): + raise ValueError("evidence-runner key is not Ed25519") + key = loaded + except (TypeError, ValueError) as exc: + raise ValueError("evidence-runner private key is invalid") from exc + return key + + def _accept_newest_product_sequence( + self, payload: ProductReleaseAdmissionPayload, artifact_sha256: str + ) -> None: + current: dict[str, object] | None = None + if self._release_state_path.exists(): + metadata = self._release_state_path.lstat() + if not stat.S_ISREG(metadata.st_mode) or ( + os.name != "nt" and stat.S_IMODE(metadata.st_mode) != 0o600 + ): + raise ValueError("product release sequence ledger is unsafe") + loaded = self._load_json(self._release_state_path) + if not isinstance(loaded, dict): + raise ValueError("product release sequence ledger is invalid") + current = loaded + if current is not None: + sequence = current.get("sequence") + digest = current.get("artifact_sha256") + if not isinstance(sequence, int) or not isinstance(digest, str): + raise ValueError("product release sequence ledger is invalid") + if payload.sequence < sequence: + raise ValueError("product release admission sequence is stale") + if payload.sequence == sequence and artifact_sha256 != digest: + raise ValueError("product release admission changed at one sequence") + if payload.sequence == sequence: + return + raw = json.dumps( + {"sequence": payload.sequence, "artifact_sha256": artifact_sha256}, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + self._release_state_path.parent.mkdir(parents=True, exist_ok=True) + tmp = self._release_state_path.with_suffix( + self._release_state_path.suffix + ".tmp" + ) + descriptor = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + try: + os.write(descriptor, raw) + os.fsync(descriptor) + finally: + os.close(descriptor) + os.replace(tmp, self._release_state_path) + if os.name != "nt": + os.chmod(self._release_state_path, 0o600) + + def _verify_product_release( + self, dispatch: HostedDispatch, config: RunnerConfig + ) -> ProductReleaseAdmissionPayload: + trust_files = config.product_release_admission + if trust_files is None: + raise ValueError("hosted runner has no product release admission trust") + raw = dispatch.product_release_admission.decode() + artifact = ProductReleaseAdmissionArtifact.model_validate_json(raw) + if ( + artifact.artifact_sha256() + != dispatch.product_release_admission.artifact_sha256 + ): + raise ValueError("product release artifact canonical digest changed") + trust = load_product_release_signer_trust( + self._load_json(trust_files.signer_registry) + ) + state = self._load_json(trust_files.state) + if not isinstance(state, dict) or set(state) != { + "newest_sequence", + "revoked_set_ids", + }: + raise ValueError("product release authority state is invalid") + newest = state["newest_sequence"] + revoked = state["revoked_set_ids"] + if ( + not isinstance(newest, int) + or not isinstance(revoked, list) + or any(not isinstance(item, str) for item in revoked) + ): + raise ValueError("product release authority state is invalid") + payload = verify_product_release_admission( + artifact, + trusted_signers=trust, + newest_sequence=newest, + revoked_set_ids=frozenset(revoked), + ) + local = {item.target: item for item in config.local_runtime_release} + if set(local) != {"flow", "desktop", "capture"}: + raise ValueError("hosted runner local release inventory is incomplete") + admitted = {item.target: item for item in payload.targets} + for target, installed in local.items(): + item = admitted[target] + if ( + installed.admission_id, + installed.admission_sha256, + installed.release_version, + installed.release_artifact_sha256, + ) != ( + item.admission_id, + item.admission_sha256, + item.release_id, + item.release_artifact_sha256, + ): + raise ValueError(f"local {target} release is not exactly admitted") + self._accept_newest_product_sequence( + payload, dispatch.product_release_admission.artifact_sha256 + ) + return payload + + def _verify_workflow_admission( + self, + dispatch: HostedDispatch, + config: RunnerConfig, + *, + evidence_private_key: Ed25519PrivateKey, + ) -> tuple[ProductionQualificationAuthority, bytes]: + trust_files = config.workflow_admission + if trust_files is None: + raise ValueError("hosted runner has no workflow admission trust") + raw = dispatch.workflow_admission.decode() + envelope = QualificationAdmissionEnvelope.model_validate_json(raw) + canonical = json.dumps( + envelope.model_dump(mode="json"), + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ).encode("utf-8") + if canonical != raw or ( + envelope.artifact_sha256() != dispatch.workflow_admission.artifact_sha256 + ): + raise ValueError("workflow admission canonical digest changed") + authorization = dispatch.payload.authorization + local_fields = tuple( + name + for name in authorization.model_fields + if name.startswith("production_qualification_") + ) + ("qualification_admission", "qualification_admission_sha256") + if any(getattr(authorization, name) is not None for name in local_fields): + raise ValueError("dispatch authorization supplies runner-local authority") + state = self._load_json(trust_files.state) + if not isinstance(state, dict) or set(state) != {"revoked_admission_ids"}: + raise ValueError("workflow admission authority state is invalid") + revoked = state["revoked_admission_ids"] + if not isinstance(revoked, list) or any( + not isinstance(item, str) for item in revoked + ): + raise ValueError("workflow admission authority state is invalid") + registry_raw = self._load_json(trust_files.signer_registry) + registry = QualificationSignerRegistry.model_validate(registry_raw) + if registry.model_dump(mode="json") != registry_raw: + raise ValueError("workflow signer registry is not canonical") + expected_raw = self._load_json(trust_files.expected_bindings) + expected = QualificationAdmissionExpected.model_validate(expected_raw) + if expected.model_dump(mode="json") != expected_raw: + raise ValueError("workflow admission expected bindings are not canonical") + + trusted = config.bundles.get(dispatch.payload.bundle.content_digest) + if trusted is None or trusted.artifact_sha256 is None: + raise ValueError("hosted bundle lacks its local artifact digest pin") + workflow = Workflow.load(trusted.path) + manifest = workflow.manifest + project = workflow.qualification + if manifest is None or project is None: + raise ValueError("hosted workflow is not sealed and qualified") + template = manifest.provenance.governed_authorization_template + if template is None: + raise ValueError("hosted workflow lacks its governed template") + profile_path = config.profiles.get(dispatch.payload.deployment_profile_id) + if profile_path is None: + raise ValueError("hosted workflow profile is not locally configured") + deployment_bytes = self._read_private_bytes( + profile_path, + maximum_bytes=1024 * 1024, + label="hosted deployment profile", + ) + deployment_sha256 = hashlib.sha256(deployment_bytes).hexdigest() + effect_contract_sha256 = contract_sha256( + [ + item.model_dump(mode="json") + for item in template.qualified_effect_requirements + ] + ) + public_key = evidence_private_key.public_key().public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + evidence_key_sha256 = evidence_runner_signer_sha256(public_key) + local_flow = next( + (item for item in config.local_runtime_release if item.target == "flow"), + None, + ) + if local_flow is None: + raise ValueError("hosted runner lacks its local Flow release binding") + local_expected = { + "tenant_id": dispatch.tenant_id, + "workflow_id": dispatch.workflow_id, + "workflow_version_id": dispatch.workflow_version_id, + "bundle_version_id": dispatch.workflow_version_id, + "bundle_artifact_sha256": trusted.artifact_sha256, + "bundle_content_digest": manifest.content_digest, + "environment_digest": project.environment.environment_digest, + "governed_authorization_template_sha256": template.template_sha256, + "environment_contract_sha256": ( + template.qualification_environment_contract_sha256 + ), + "input_policy_sha256": template.parameter_contract_sha256, + "action_policy_sha256": template.qualification_project_contract_sha256, + "identity_contract_sha256": template.identity_contract_sha256, + "effect_contract_sha256": effect_contract_sha256, + "evidence_runner_signer_sha256": evidence_key_sha256, + "deployment_manifest_sha256": deployment_sha256, + } + mismatches = sorted( + name + for name, value in local_expected.items() + if getattr(expected, name) != value + ) + runtime = expected.runtime_build_identity + if ( + runtime.flow_version != local_flow.release_version + or runtime.flow_wheel_sha256 != local_flow.release_artifact_sha256 + ): + mismatches.append("runtime_build_identity") + if mismatches: + raise ValueError( + "local workflow admission expectation differs from live state: " + + ", ".join(sorted(set(mismatches))) + ) + verify_qualification_admission( + envelope, + registry=registry, + expected=expected, + revoked_admission_ids=frozenset(revoked), + ) + return ( + ProductionQualificationAuthority( + qualification_admission=envelope, + qualification_admission_sha256=envelope.artifact_sha256(), + expected=expected, + qualification_signer_registry=registry, + qualification_signer_registry_sha256=registry.artifact_sha256(), + permit_trust_snapshot=None, + revoked_admission_ids=tuple(sorted(set(revoked))), + ), + deployment_bytes, + ) + + @staticmethod + def _write_private_json( + path: Path, value: BaseModel | Mapping[str, object] + ) -> Path: + payload: object = ( + value.model_dump(mode="json") if isinstance(value, BaseModel) else value + ) + raw = json.dumps( + payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + descriptor = os.open(path, flags, 0o600) + try: + os.write(descriptor, raw) + os.fsync(descriptor) + finally: + os.close(descriptor) + return path + + @staticmethod + def _write_private_bytes(path: Path, raw: bytes) -> Path: + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + descriptor = os.open(path, flags, 0o600) + try: + offset = 0 + while offset < len(raw): + written = os.write(descriptor, raw[offset:]) + if written <= 0: + raise OSError("protected file write did not make progress") + offset += written + os.fsync(descriptor) + finally: + os.close(descriptor) + return path + + def _resolve_params( + self, + dispatch: HostedDispatch, + config: RunnerConfig, + ) -> dict[str, str]: + trusted = config.bundles.get(dispatch.payload.bundle.content_digest) + if trusted is None: + raise ValueError("hosted bundle is not locally trusted") + workflow = Workflow.load(trusted.path) + if isinstance(dispatch.payload.params, DispatchParamsValues): + supplied = dict(dispatch.payload.params.values) + inline = True + else: + root = config.params_ref_root + if root is None: + raise ValueError("parameter reference has no protected local root") + ref = dispatch.payload.params.ref + parsed_url = urlsplit(ref) + relative = PurePosixPath(ref) + if ( + parsed_url.scheme + or parsed_url.netloc + or parsed_url.query + or parsed_url.fragment + or relative.is_absolute() + or not relative.parts + or any(part in {"", ".", ".."} for part in relative.parts) + or "\\" in ref + ): + raise ValueError("parameter reference is not a safe local path") + root_stat = root.lstat() + if ( + not stat.S_ISDIR(root_stat.st_mode) + or stat.S_ISLNK(root_stat.st_mode) + or ( + os.name != "nt" + and ( + root_stat.st_uid != os.geteuid() + or stat.S_IMODE(root_stat.st_mode) & 0o077 + ) + ) + ): + raise ValueError("parameter reference root is not protected") + current = root + for component in relative.parts[:-1]: + current /= component + component_stat = current.lstat() + if ( + not stat.S_ISDIR(component_stat.st_mode) + or stat.S_ISLNK(component_stat.st_mode) + or ( + os.name != "nt" + and ( + component_stat.st_uid != os.geteuid() + or stat.S_IMODE(component_stat.st_mode) & 0o077 + ) + ) + ): + raise ValueError("parameter reference traverses an unsafe path") + raw = self._read_private_bytes( + root.joinpath(*relative.parts), + maximum_bytes=256 * 1024, + label="parameter reference", + ) + try: + supplied = json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ValueError("parameter reference is not valid JSON") from exc + if not isinstance(supplied, dict) or any( + not isinstance(key, str) or not isinstance(value, str) + for key, value in supplied.items() + ): + raise ValueError("parameter reference has an invalid exact shape") + inline = False + return resolve_admitted_params(workflow, supplied, inline=inline) + + @staticmethod + def _write_params(path: Path, params: dict[str, str]) -> Path | None: + if not params: + return None + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + descriptor = os.open(path, flags, 0o600) + try: + raw = json.dumps(params, sort_keys=True, separators=(",", ":")).encode() + os.write(descriptor, raw) + os.fsync(descriptor) + finally: + os.close(descriptor) + return path + + @staticmethod + def _validate_terminal( + dispatch: HostedDispatch, + report_bytes: bytes, + proof: ProductionTerminalVerificationEnvelope, + ) -> None: + del dispatch, report_bytes, proof + raise ValueError( + "hosted production success requires a locally produced proof from " + "independent expected state and retained delivery receipts" + ) + + @staticmethod + def _refusal( + dispatch: HostedDispatch | None, code: str, detail: str + ) -> HostedDispatchRefusal: + events: tuple[dict[str, Any], ...] = () + if dispatch is not None: + refusal = Refusal(RefusalCode.MALFORMED_DISPATCH, detail[:300]) + events = tuple( + refusal_events( + refusal, + run_id=dispatch.run_id, + workflow_id=dispatch.workflow_id, + bundle_digest=dispatch.payload.bundle.content_digest, + authorization_id=dispatch.payload.authorization.authorization_id, + ) + ) + return HostedDispatchRefusal( + dispatch_id=dispatch.dispatch_id if dispatch else None, + run_id=dispatch.run_id if dispatch else None, + code=code, + detail=detail[:400], + evidence_batch=events, + ) + + @staticmethod + def recovery_binding(dispatch: HostedDispatch) -> HostedRecoveryBinding: + """Project the exact credential-bearing state needed after a crash.""" + + return HostedRecoveryBinding( + dispatch_id=dispatch.dispatch_id, + runner_session_id=dispatch.runner_session_id, + dispatch_session_id=dispatch.dispatch_session_id, + run_id=dispatch.run_id, + workflow_id=dispatch.workflow_id, + idempotency_key=dispatch.idempotency_key, + lease_token=dispatch.lease_token, + product_release_admission_sha256=( + dispatch.product_release_admission.artifact_sha256 + ), + workflow_admission_sha256=dispatch.workflow_admission.artifact_sha256, + bundle_content_digest=dispatch.payload.bundle.content_digest, + authorization_id=dispatch.payload.authorization.authorization_id, + ) + + def reconciliation_required( + self, + binding: HostedRecoveryBinding | Mapping[str, object], + *, + code: str = "runner_result_lost", + ) -> HostedRunResult: + """Close a crash window without re-entering the execution path.""" + + parsed = HostedRecoveryBinding.model_validate(binding) + if ( + not code + or len(code) > 64 + or any( + character not in "abcdefghijklmnopqrstuvwxyz0123456789_" + for character in code + ) + ): + raise ValueError("reconciliation code is invalid") + return HostedRunResult( + dispatch_id=parsed.dispatch_id, + run_id=parsed.run_id, + outcome=TransactionOutcome.RECONCILIATION_REQUIRED, + evidence_batch=tuple( + failure_events( + run_id=parsed.run_id, + bundle_digest=parsed.bundle_content_digest, + authorization_id=parsed.authorization_id, + ) + ), + started=True, + uncertain_delivery=True, + report_sha256="0" * 64, + ) + + def execute( + self, + dispatch: HostedDispatch | Mapping[str, object], + *, + runner_config: Path, + run_dir: Path, + authority: DeliveryAuthority, + ) -> Union[HostedRunResult, HostedDispatchRefusal]: + parsed: HostedDispatch | None = None + try: + parsed = HostedDispatch.model_validate(dispatch) + expiry = _utc_seconds(parsed.lease_expires_at, label="hosted lease expiry") + if datetime.now(timezone.utc) >= expiry: + raise ValueError("hosted lease expired before execution") + if ( + authority.url != parsed.managed_delivery_authority_url + or authority.token != parsed.delivery_authority_token + ): + raise ValueError("delivery authority does not match the hosted lease") + config = load_runner_config(runner_config, protected=True) + configured_origin = self._protected_runner_origin(config) + authority_host = urlsplit(authority.url) + if ( + configured_origin + != f"{authority_host.scheme}://{authority_host.netloc}" + ): + raise ValueError( + "delivery authority origin differs from the protected runner host" + ) + self._verify_product_release(parsed, config) + evidence_private_key = self._load_evidence_private_key(config) + qualification, deployment_bytes = self._verify_workflow_admission( + parsed, + config, + evidence_private_key=evidence_private_key, + ) + params = self._resolve_params(parsed, config) + run_dir = Path(run_dir) + run_dir.mkdir(parents=True, exist_ok=False, mode=0o700) + if os.name != "nt": + run_dir.chmod(0o700) + run_dir_stat = run_dir.lstat() + if ( + not stat.S_ISDIR(run_dir_stat.st_mode) + or stat.S_ISLNK(run_dir_stat.st_mode) + or ( + os.name != "nt" + and ( + run_dir_stat.st_uid != os.geteuid() + or stat.S_IMODE(run_dir_stat.st_mode) != 0o700 + ) + ) + ): + raise ValueError("hosted run directory is not protected") + profile_path = self._write_private_bytes( + run_dir / "deployment.yaml", deployment_bytes + ) + staged_deployment_bytes = self._read_private_bytes( + profile_path, + maximum_bytes=1024 * 1024, + label="staged hosted deployment profile", + ) + if staged_deployment_bytes != deployment_bytes: + raise ValueError("staged hosted deployment profile changed") + staged_profiles = dict(config.profiles) + staged_profiles[parsed.payload.deployment_profile_id] = profile_path + staged_config = replace(config, profiles=staged_profiles) + verified = verify_dispatch( + parsed.payload, + staged_config, + resolved_params=params, + ) + if isinstance(verified, Refusal): + return self._refusal(parsed, verified.code.value, verified.reason()) + except Exception as exc: + return self._refusal( + parsed, + "hosted_admission_refused", + f"prestart_{type(exc).__name__}", + ) + + reservation_key = f"{parsed.tenant_id}:{parsed.idempotency_key}" + try: + self._ledger.reserve(reservation_key, run_id=parsed.run_id) + except DuplicateActuation: + return self.reconciliation_required( + self.recovery_binding(parsed), code="dispatch_already_consumed" + ) + + try: + params_file = self._write_params(run_dir / "params.json", params) + qualification_authority_file = self._write_private_json( + run_dir / "qualification-authority.json", qualification + ) + guard = ProductionQualificationGuard( + qualification_authority_file, + remote_permit_revalidation=True, + ) + production_binding = guard.authorization_binding(verified.workflow) + local_authorization = verified.payload.authorization.model_copy( + update=production_binding + ) + local_payload = verified.payload.model_copy( + update={"authorization": local_authorization} + ) + verified = replace(verified, payload=local_payload) + dispatch_file = write_managed_dispatch_envelope( + run_dir / "managed-dispatch.json", verified + ) + argv = build_run_argv( + verified, + run_dir, + params_file, + managed_dispatch_file=dispatch_file, + qualification_authority_file=qualification_authority_file, + ) + child_env = os.environ.copy() + child_env.pop(REMOTE_AUTHORITY_URL_ENV, None) + child_env.pop(REMOTE_AUTHORITY_TOKEN_ENV, None) + child_env.pop(REMOTE_DISPATCH_SESSION_ID_ENV, None) + child_env.update(authority.child_environment()) + child_env[REMOTE_DISPATCH_SESSION_ID_ENV] = parsed.dispatch_session_id + except Exception: # preparation failed before the managed child started + self._ledger.record_outcome( + reservation_key, + TransactionOutcome.FAILED_PLATFORM, + run_id=parsed.run_id, + ) + events = tuple( + failure_events( + run_id=parsed.run_id, + bundle_digest=parsed.payload.bundle.content_digest, + authorization_id=parsed.payload.authorization.authorization_id, + ) + ) + return HostedRunResult( + dispatch_id=parsed.dispatch_id, + run_id=parsed.run_id, + outcome=TransactionOutcome.FAILED_PLATFORM, + evidence_batch=events, + started=False, + uncertain_delivery=False, + report_sha256="0" * 64, + ) + + try: + # Once this call starts, the child can reach a real input edge. Any + # lost or malformed result is uncertain and can never be retried. + execution = self._runner(argv, run_dir, child_env) + except Exception: + outcome = TransactionOutcome.RECONCILIATION_REQUIRED + self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) + return HostedRunResult( + dispatch_id=parsed.dispatch_id, + run_id=parsed.run_id, + outcome=outcome, + evidence_batch=tuple( + failure_events( + run_id=parsed.run_id, + bundle_digest=parsed.payload.bundle.content_digest, + authorization_id=parsed.payload.authorization.authorization_id, + ) + ), + started=True, + uncertain_delivery=True, + report_sha256="0" * 64, + ) + + if execution.report_bytes is None: + outcome = TransactionOutcome.RECONCILIATION_REQUIRED + self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) + return HostedRunResult( + dispatch_id=parsed.dispatch_id, + run_id=parsed.run_id, + outcome=outcome, + evidence_batch=tuple( + failure_events( + run_id=parsed.run_id, + bundle_digest=parsed.payload.bundle.content_digest, + authorization_id=parsed.payload.authorization.authorization_id, + ) + ), + started=True, + uncertain_delivery=True, + report_sha256="0" * 64, + ) + report: RunReport | None = None + try: + report = RunReport.model_validate_json(execution.report_bytes) + outcome = classify_transaction_outcome(report) + proof = execution.terminal_verification + if outcome is TransactionOutcome.VERIFIED: + if proof is None: + outcome = TransactionOutcome.RECONCILIATION_REQUIRED + else: + self._validate_terminal(parsed, execution.report_bytes, proof) + elif proof is not None: + raise ValueError("non-VERIFIED execution supplied a success proof") + except ValueError: + outcome = TransactionOutcome.RECONCILIATION_REQUIRED + proof = None + if outcome is not TransactionOutcome.VERIFIED: + proof = None + report_digest = hashlib.sha256(execution.report_bytes).hexdigest() + self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) + if report is None: + events = tuple( + failure_events( + run_id=parsed.run_id, + bundle_digest=parsed.payload.bundle.content_digest, + authorization_id=parsed.payload.authorization.authorization_id, + ) + ) + else: + events = tuple( + report_events( + report, + run_id=parsed.run_id, + workflow_id=parsed.workflow_id, + bundle_digest=parsed.payload.bundle.content_digest, + authorization_id=parsed.payload.authorization.authorization_id, + consequential_steps=verified.consequential_steps, + effect_covered_consequential_steps=( + verified.effect_covered_consequential_steps + ), + ) + ) + uncertain = outcome is TransactionOutcome.RECONCILIATION_REQUIRED or any( + result.delivery_uncertainty is not None + for result in (report.results if report is not None else ()) + ) + return HostedRunResult( + dispatch_id=parsed.dispatch_id, + run_id=parsed.run_id, + outcome=outcome, + evidence_batch=events, + terminal_verification=proof, + started=True, + uncertain_delivery=uncertain, + report_sha256=report_digest, + ) + + def callback_request( + self, + dispatch: HostedDispatch, + result: HostedRunResult | HostedDispatchRefusal, + ) -> CallbackRequest: + if ( + result.dispatch_id != dispatch.dispatch_id + or result.run_id != dispatch.run_id + ): + raise ValueError("hosted result does not bind the callback lease") + events = list(result.evidence_batch) + proof_bytes = None + proof_digest = None + if isinstance(result, HostedRunResult): + if result.terminal_verification is not None: + raise ValueError( + "the full local v2 proof cannot cross the hosted callback boundary" + ) + terminal = HostedTerminalEvent( + run_id=dispatch.run_id, + outcome=( + result.outcome.value + if isinstance(result.outcome, TransactionOutcome) + else result.outcome + ), + report_sha256=result.report_sha256, + started=result.started, + uncertain_delivery=result.uncertain_delivery, + terminal_verification_artifact_bytes_base64=proof_bytes, + terminal_verification_artifact_sha256=proof_digest, + ) + events.append(terminal.model_dump(mode="json")) + return CallbackRequest( + dispatch_id=dispatch.dispatch_id, + runner_session_id=dispatch.runner_session_id, + idempotency_key=dispatch.idempotency_key, + lease_token=dispatch.lease_token, + product_release_admission_sha256=( + dispatch.product_release_admission.artifact_sha256 + ), + workflow_admission_sha256=dispatch.workflow_admission.artifact_sha256, + events=tuple(events), + ) diff --git a/openadapt_flow/runner/inputs.py b/openadapt_flow/runner/inputs.py new file mode 100644 index 00000000..977fdab8 --- /dev/null +++ b/openadapt_flow/runner/inputs.py @@ -0,0 +1,95 @@ +"""Fail-closed resolution of hosted values from the sealed input schema.""" + +from __future__ import annotations + +import math +from datetime import date + +from openadapt_flow.ir import ParamKind, ParamSpec, Workflow +from openadapt_flow.runtime.authorization import effective_runtime_params + + +class AdmittedInputError(ValueError): + """Hosted inputs do not fit the exact schema sealed into the workflow.""" + + +def _validate_value(spec: ParamSpec, value: str) -> None: + if spec.type is ParamKind.ENUM: + if not spec.choices or value not in spec.choices: + raise AdmittedInputError( + f"parameter {spec.name!r} is outside its admitted enum" + ) + elif spec.type is ParamKind.DATE: + try: + parsed = date.fromisoformat(value) + except ValueError as exc: + raise AdmittedInputError( + f"parameter {spec.name!r} is not an ISO date" + ) from exc + if parsed.isoformat() != value: + raise AdmittedInputError( + f"parameter {spec.name!r} is not a canonical ISO date" + ) + elif spec.type is ParamKind.NUMBER: + try: + number = float(value) + except ValueError as exc: + raise AdmittedInputError( + f"parameter {spec.name!r} is not a number" + ) from exc + if not math.isfinite(number): + raise AdmittedInputError(f"parameter {spec.name!r} must be a finite number") + + +def resolve_admitted_params( + workflow: Workflow, + supplied: dict[str, str], + *, + inline: bool, +) -> dict[str, str]: + """Resolve exact hosted params without inventing or widening the schema. + + Hosted execution requires the sealed typed schema. Inline input can never + carry a declared secret. Values obtained through a customer-local reference + resolver may carry a secret, but they still pass the same exact name and + type checks before the authorization digest is recomputed. + """ + + if not workflow.param_specs: + raise AdmittedInputError( + "hosted execution requires a sealed typed parameter schema" + ) + if any(name != spec.name for name, spec in workflow.param_specs.items()): + raise AdmittedInputError("the sealed parameter schema has a name mismatch") + + admitted_names = set(workflow.param_specs) + unknown = sorted(set(supplied).difference(admitted_names)) + if unknown: + raise AdmittedInputError( + "hosted input contains parameter(s) outside the admitted schema: " + + ", ".join(unknown) + ) + if inline: + inline_secrets = sorted(set(supplied).intersection(workflow.secret_params)) + if inline_secrets: + raise AdmittedInputError( + "inline hosted input contains declared secret parameter(s): " + + ", ".join(inline_secrets) + ) + + resolved = effective_runtime_params(workflow, supplied) + unknown_defaults = sorted(set(resolved).difference(admitted_names)) + if unknown_defaults: + raise AdmittedInputError( + "workflow defaults fall outside the admitted parameter schema" + ) + for name, spec in sorted(workflow.param_specs.items()): + value = resolved.get(name) + if value is None: + if spec.required: + raise AdmittedInputError(f"required parameter {name!r} is missing") + continue + if not isinstance(value, str): + raise AdmittedInputError(f"parameter {name!r} is not a string value") + _validate_value(spec, value) + return resolved diff --git a/openadapt_flow/runner/product_release.py b/openadapt_flow/runner/product_release.py new file mode 100644 index 00000000..22db9849 --- /dev/null +++ b/openadapt_flow/runner/product_release.py @@ -0,0 +1,267 @@ +"""Verification of the signed seven-target Product release admission.""" + +from __future__ import annotations + +import hashlib +import json +import re +from base64 import b64decode, urlsafe_b64decode, urlsafe_b64encode +from datetime import datetime, timezone +from typing import Literal, Mapping + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +DOMAIN = b"openadapt.product-release-admission-payload.v1\0" +TARGETS = ("agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt") +_HEX64 = r"^[a-f0-9]{64}$" +_UUID = r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" +_KEY_ID = r"^release-admission-ed25519-[a-f0-9]{16}$" +_SAFE_ID = r"^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,199}$" +_UTC = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$") + + +class ProductReleaseAdmissionError(ValueError): + """The aggregate admission is invalid or inactive.""" + + +class _Closed(BaseModel): + model_config = ConfigDict(strict=True, extra="forbid", frozen=True) + + +def _canonical_json(value: object) -> bytes: + if isinstance(value, BaseModel): + value = value.model_dump(mode="json") + return json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + + +def _utc(value: str, *, field: str) -> datetime: + if _UTC.fullmatch(value) is None: + raise ValueError(f"{field} is not canonical UTC seconds") + try: + parsed = datetime.fromisoformat(value[:-1] + "+00:00") + except ValueError as exc: + raise ValueError(f"{field} is not UTC seconds") from exc + if parsed.tzinfo is None or parsed.microsecond != 0: + raise ValueError(f"{field} is not UTC seconds") + return parsed.astimezone(timezone.utc) + + +class ProductReleaseTarget(_Closed): + target: Literal["agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt"] + admission_id: str = Field(pattern=_UUID) + admission_sha256: str = Field(pattern=_HEX64) + release_id: str = Field(pattern=_SAFE_ID) + release_artifact_sha256: str = Field(pattern=_HEX64) + admission_issued_at: str + admission_expires_at: str + revoked_at: str | None + artifact_authority_sha256: str = Field(pattern=_HEX64) + artifact_authority_state: Literal["active", "revoked", "expired", "unavailable"] + artifact_authority_checked_at: str + artifact_authority_expires_at: str + + @model_validator(mode="after") + def _chronology(self) -> "ProductReleaseTarget": + issued = _utc(self.admission_issued_at, field="target admission_issued_at") + expires = _utc(self.admission_expires_at, field="target admission_expires_at") + checked = _utc( + self.artifact_authority_checked_at, + field="target artifact_authority_checked_at", + ) + authority_expires = _utc( + self.artifact_authority_expires_at, + field="target artifact_authority_expires_at", + ) + if issued >= expires or checked >= authority_expires: + raise ValueError("product release target chronology is invalid") + if self.revoked_at is not None: + _utc(self.revoked_at, field="target revoked_at") + return self + + +class ProductReleaseAdmissionPayload(_Closed): + schema_version: Literal["openadapt.product-release-admission-payload/v1"] + set_id: str = Field(pattern=_UUID) + sequence: int = Field(gt=0, le=9_007_199_254_740_991) + policy_sha256: str = Field(pattern=_HEX64) + issued_at: str + expires_at: str + targets: tuple[ProductReleaseTarget, ...] = Field(min_length=7, max_length=7) + + @model_validator(mode="after") + def _closed_set(self) -> "ProductReleaseAdmissionPayload": + issued = _utc(self.issued_at, field="product admission issued_at") + expires = _utc(self.expires_at, field="product admission expires_at") + if issued >= expires: + raise ValueError("product release admission chronology is invalid") + if tuple(item.target for item in self.targets) != TARGETS: + raise ValueError( + "product release admission targets are not exact and ordered" + ) + return self + + def canonical_bytes(self) -> bytes: + return _canonical_json(self) + + def payload_sha256_value(self) -> str: + return hashlib.sha256(DOMAIN + self.canonical_bytes()).hexdigest() + + +class ProductReleaseSigner(_Closed): + algorithm: Literal["ed25519"] + key_id: str = Field(pattern=_KEY_ID) + public_key: str + + @field_validator("public_key") + @classmethod + def _key(cls, value: str) -> str: + try: + raw = b64decode(value, validate=True) + except ValueError as exc: + raise ValueError("product release signer key is invalid") from exc + if len(raw) != 32: + raise ValueError("product release signer key is invalid") + return value + + @model_validator(mode="after") + def _key_id_matches(self) -> "ProductReleaseSigner": + raw = b64decode(self.public_key, validate=True) + expected = "release-admission-ed25519-" + hashlib.sha256(raw).hexdigest()[:16] + if self.key_id != expected: + raise ValueError("product release signer key id is invalid") + return self + + +class ProductReleaseAdmissionArtifact(_Closed): + schema_version: Literal["openadapt.product-release-admission-artifact/v1"] + payload: ProductReleaseAdmissionPayload + payload_sha256: str = Field(pattern=_HEX64) + signer: ProductReleaseSigner + signature: str = Field(min_length=86, max_length=86) + + @model_validator(mode="after") + def _self_consistent(self) -> "ProductReleaseAdmissionArtifact": + if self.payload_sha256 != self.payload.payload_sha256_value(): + raise ValueError("product release admission payload digest is invalid") + try: + signature = urlsafe_b64decode(self.signature + "==") + except ValueError as exc: + raise ValueError("product release admission signature is invalid") from exc + if ( + len(signature) != 64 + or urlsafe_b64encode(signature).decode("ascii").rstrip("=") + != self.signature + ): + raise ValueError("product release admission signature is invalid") + try: + Ed25519PublicKey.from_public_bytes( + b64decode(self.signer.public_key, validate=True) + ).verify(signature, DOMAIN + self.payload.canonical_bytes()) + except (InvalidSignature, ValueError) as exc: + raise ValueError("product release admission signature is invalid") from exc + return self + + def artifact_sha256(self) -> str: + return hashlib.sha256(_canonical_json(self)).hexdigest() + + +class ProductReleaseSignerTrust(_Closed): + public_key: str + status: Literal["active", "revoked"] + revoked_at: str | None + + @model_validator(mode="after") + def _state(self) -> "ProductReleaseSignerTrust": + if self.status == "active" and self.revoked_at is not None: + raise ValueError("active product release signer has a revocation time") + if self.status == "revoked" and self.revoked_at is None: + raise ValueError("revoked product release signer lacks a revocation time") + if self.revoked_at is not None: + _utc(self.revoked_at, field="product release signer revoked_at") + return self + + +def verify_product_release_admission( + artifact: ProductReleaseAdmissionArtifact, + *, + trusted_signers: Mapping[str, ProductReleaseSignerTrust], + newest_sequence: int, + revoked_set_ids: set[str] | frozenset[str] = frozenset(), + now: datetime | None = None, +) -> ProductReleaseAdmissionPayload: + """Verify signature, authority state, time, revocation, and newest sequence.""" + + try: + artifact = ProductReleaseAdmissionArtifact.model_validate_json( + _canonical_json(artifact) + ) + except ValueError as exc: + raise ProductReleaseAdmissionError(str(exc)) from exc + trust = trusted_signers.get(artifact.signer.key_id) + if trust is None or trust.public_key != artifact.signer.public_key: + raise ProductReleaseAdmissionError("product release signer is not trusted") + if trust.status != "active": + raise ProductReleaseAdmissionError("product release signer is revoked") + payload = artifact.payload + if payload.set_id in revoked_set_ids: + raise ProductReleaseAdmissionError("product release admission is revoked") + if payload.sequence != newest_sequence: + raise ProductReleaseAdmissionError("product release admission is superseded") + current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc) + if ( + not _utc(payload.issued_at, field="issued_at") + <= current + < _utc(payload.expires_at, field="expires_at") + ): + raise ProductReleaseAdmissionError("product release admission is not active") + for target in payload.targets: + if target.revoked_at is not None: + raise ProductReleaseAdmissionError( + f"product release target {target.target} is revoked" + ) + if target.artifact_authority_state != "active": + raise ProductReleaseAdmissionError( + f"product release target {target.target} authority is not active" + ) + if ( + not _utc(target.admission_issued_at, field="target issued_at") + <= current + < _utc(target.admission_expires_at, field="target expires_at") + ): + raise ProductReleaseAdmissionError( + f"product release target {target.target} admission is not active" + ) + if ( + not _utc(target.artifact_authority_checked_at, field="authority checked_at") + <= current + < _utc(target.artifact_authority_expires_at, field="authority expires_at") + ): + raise ProductReleaseAdmissionError( + f"product release target {target.target} authority is stale" + ) + return payload + + +def load_product_release_signer_trust( + raw: object, +) -> dict[str, ProductReleaseSignerTrust]: + if not isinstance(raw, dict) or not raw: + raise ProductReleaseAdmissionError( + "product release signer trust is unavailable" + ) + try: + parsed = { + str(key): ProductReleaseSignerTrust.model_validate(value) + for key, value in raw.items() + } + except ValueError as exc: + raise ProductReleaseAdmissionError( + "product release signer trust is invalid" + ) from exc + if any(re.fullmatch(_KEY_ID, key) is None for key in parsed): + raise ProductReleaseAdmissionError("product release signer key id is invalid") + return parsed diff --git a/openadapt_flow/runner/protocol.py b/openadapt_flow/runner/protocol.py index f469b603..be85f9f5 100644 --- a/openadapt_flow/runner/protocol.py +++ b/openadapt_flow/runner/protocol.py @@ -43,6 +43,8 @@ #: v2 permit carries its own admission and authority digests. DISPATCH_BINDING_LOCAL_FIELDS = frozenset( { + "qualification_admission", + "qualification_admission_sha256", "production_qualification_admission_id", "production_qualification_admission_sha256", "production_qualification_evidence_identity_sha256", @@ -112,9 +114,13 @@ class DispatchParamsValues(BaseModel): class DispatchParamsRef(BaseModel): - """Regulated-lane params-by-reference. Parsed, but refused in v1: the - local reference resolver does not exist yet, and guessing values would - break the runtime-inputs digest binding.""" + """Regulated-lane reference to protected customer-local parameters. + + The hosted adapter treats ``ref`` only as a relative path under the + operator-configured protected root. It refuses URLs, traversal, and links, + then checks ``expected_digest`` after typed local resolution. Resolved + values stay local and never enter the callback. + """ model_config = ConfigDict(frozen=True, extra="forbid") @@ -133,7 +139,7 @@ class RunnerDispatchPayload(BaseModel): # exact UUID across runner restart and reassignment. run_id: str = Field( pattern=( - "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + "^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" ) ) workflow_id: str diff --git a/openadapt_flow/runner/verify.py b/openadapt_flow/runner/verify.py index 6e644387..8819b0b1 100644 --- a/openadapt_flow/runner/verify.py +++ b/openadapt_flow/runner/verify.py @@ -143,6 +143,7 @@ def verify_dispatch( *, now: Optional[datetime] = None, active_workflow_ids: Optional[set[str]] = None, + resolved_params: Optional[dict[str, str]] = None, ) -> VerifiedDispatch | Refusal: """Independently verify ``payload`` against local trust. Never executes. @@ -194,18 +195,47 @@ def verify_dispatch( "this runner never downloads bundles", ) - if not isinstance(payload.params, DispatchParamsValues): - return Refusal( - RefusalCode.PARAMS_REF_UNSUPPORTED, - "params-by-reference (regulated lane) has no local resolver in v1", - ) - if trusted.params_ref_required: + from openadapt_flow.ir import Workflow + + try: + workflow = Workflow.load(trusted.path) + except Exception as exc: # noqa: BLE001 - crypto/integrity/shape: refuse return Refusal( - RefusalCode.PARAMS_VALUES_REFUSED, - "this bundle requires params-by-reference; inline params.values " - "dispatches are refused on this machine", + RefusalCode.BUNDLE_LOAD_FAILED, + f"trusted bundle failed to load: {type(exc).__name__}", ) - params = dict(payload.params.values) + + if isinstance(payload.params, DispatchParamsValues): + if trusted.params_ref_required: + return Refusal( + RefusalCode.PARAMS_VALUES_REFUSED, + "this bundle requires params-by-reference; inline params.values " + "dispatches are refused on this machine", + ) + params = dict(payload.params.values) + if resolved_params is not None and resolved_params != params: + return Refusal( + RefusalCode.RUNTIME_INPUTS_MISMATCH, + "locally resolved params differ from inline dispatch values", + ) + else: + if resolved_params is None: + return Refusal( + RefusalCode.PARAMS_REF_UNSUPPORTED, + "params-by-reference requires an explicit local resolver", + ) + params = dict(resolved_params) + from openadapt_flow.runtime.authorization import runtime_inputs_digest + + resolved_digest = runtime_inputs_digest(workflow, params, None) + if ( + resolved_digest != payload.params.expected_digest + or resolved_digest != payload.authorization.runtime_inputs_digest + ): + return Refusal( + RefusalCode.RUNTIME_INPUTS_MISMATCH, + "locally resolved parameter values do not match the admitted digest", + ) if trusted.param_patterns: for key in sorted(params): @@ -246,16 +276,6 @@ def verify_dispatch( "contract requires screenshots_may_leave_box=false", ) - from openadapt_flow.ir import Workflow - - try: - workflow = Workflow.load(trusted.path) - except Exception as exc: # noqa: BLE001 - crypto/integrity/shape: refuse - return Refusal( - RefusalCode.BUNDLE_LOAD_FAILED, - f"trusted bundle failed to load: {type(exc).__name__}", - ) - fit_refusal = payload.authorization.validate_workflow(workflow) if fit_refusal is not None: return Refusal(RefusalCode.AUTHORIZATION_MISMATCH, fit_refusal) diff --git a/openadapt_flow/runtime/durable/authority.py b/openadapt_flow/runtime/durable/authority.py index 591853c8..dde650d8 100644 --- a/openadapt_flow/runtime/durable/authority.py +++ b/openadapt_flow/runtime/durable/authority.py @@ -52,9 +52,10 @@ AUTHORITY_DB_ENV = "OPENADAPT_DURABLE_AUTHORITY_DB" REMOTE_AUTHORITY_URL_ENV = "OPENADAPT_DURABLE_AUTHORITY_URL" -# Reuse the enrolled runner credential. The operator configures one trust -# relationship with the control plane, not a second delivery-only secret. +# The managed parent injects a run-scoped delivery-authority credential. Keep +# the established environment name for child-runtime compatibility. REMOTE_AUTHORITY_TOKEN_ENV = "OPENADAPT_RUNNER_TOKEN" +REMOTE_DISPATCH_SESSION_ID_ENV = "OPENADAPT_DURABLE_DISPATCH_SESSION_ID" # This observer exists only for the closed synthetic Execute acceptance run. # A Modal launcher owns the fixed, pre-opened non-blocking pipe at descriptor # three. A bundle, CLI invocation, or remote caller cannot select a path, @@ -70,9 +71,8 @@ JOURNAL_GENESIS_DIGEST = "sha256:" + hashlib.sha256(b"").hexdigest() JOURNAL_MAC_DOMAIN = b"openadapt-attended-journal-v1\0" _REMOTE_UUID_RE = re.compile( - r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-" - r"[89ab][0-9a-f]{3}-[0-9a-f]{12}", - re.IGNORECASE, + r"[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-" + r"[89ab][0-9a-f]{3}-[0-9a-f]{12}" ) _REMOTE_AUTHORITY_ID_RE = _REMOTE_UUID_RE _REMOTE_TOKEN_RE = re.compile(r"[a-f0-9]{32}") @@ -2006,6 +2006,7 @@ def _require_remote_delivery_permit( ) url = os.getenv(REMOTE_AUTHORITY_URL_ENV, "") token = os.getenv(REMOTE_AUTHORITY_TOKEN_ENV, "") + expected_dispatch_session_id = os.getenv(REMOTE_DISPATCH_SESSION_ID_ENV, "") if not url or not token: raise DurableAuthorityBusy( "production delivery requires configured remote authority credentials" @@ -2121,6 +2122,10 @@ def _require_remote_delivery_permit( and _REMOTE_UUID_RE.fullmatch(response["permit_id"]) and isinstance(response["dispatch_session_id"], str) and _REMOTE_UUID_RE.fullmatch(response["dispatch_session_id"]) + and ( + not expected_dispatch_session_id + or response["dispatch_session_id"] == expected_dispatch_session_id + ) and isinstance(response["one_use_claim_id"], str) and _REMOTE_UUID_RE.fullmatch(response["one_use_claim_id"]) and isinstance(response["permit_artifact_sha256"], str) diff --git a/tests/test_durable_authority_v13.py b/tests/test_durable_authority_v13.py index a9240dea..f484327e 100644 --- a/tests/test_durable_authority_v13.py +++ b/tests/test_durable_authority_v13.py @@ -38,6 +38,7 @@ AUTHORITY_DB_ENV, REMOTE_AUTHORITY_TOKEN_ENV, REMOTE_AUTHORITY_URL_ENV, + REMOTE_DISPATCH_SESSION_ID_ENV, SYNTHETIC_DELIVERY_MARKER_ENABLED_ENV, SYNTHETIC_DELIVERY_MARKER_RUN_ID_ENV, DurableAuthority, @@ -534,6 +535,52 @@ def test_v2_permit_remains_pending_until_signed_receipt_commits_edge( assert entry.runtime_delivery_sequence == 0 +def test_remote_permit_refuses_a_different_hosted_dispatch_session( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + transport = _issued_permit_transport("wrong-session", "4" * 64) + manifest, authority = _remote_initial_authority(tmp_path, monkeypatch, transport) + monkeypatch.setenv( + REMOTE_DISPATCH_SESSION_ID_ENV, + "30000000-0000-4000-8000-000000000002", + ) + + with pytest.raises(DurableAuthorityBusy, match="does not match request"): + authority.before_initial_delivery(manifest) + + assert authority.validate(manifest).delivery_sequence == 0 + + +def test_lost_delivery_acknowledgment_response_blocks_replay_dispatch( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + issued = _issued_permit_transport("lost-ack", "5" * 64) + permit_calls = 0 + acknowledgment_calls = 0 + + def transport(url: str, headers: dict[str, str], body: bytes) -> bytes: + nonlocal permit_calls, acknowledgment_calls + response = issued(url, headers, body) + if url.endswith("/managed-delivery-acknowledgment"): + acknowledgment_calls += 1 + raise TimeoutError("acknowledgment response lost after server commit") + permit_calls += 1 + return response + + manifest, authority = _remote_initial_authority(tmp_path, monkeypatch, transport) + permit = authority.before_initial_delivery(manifest) + assert permit is not None + + with pytest.raises(DurableAuthorityBusy, match="unavailable or refused"): + authority.acknowledge_remote_delivery(manifest, permit) + with pytest.raises(DurableAuthorityBusy, match="lacks an acknowledgment"): + authority.before_initial_delivery(manifest) + + assert permit_calls == 1 + assert acknowledgment_calls == 1 + assert authority.validate(manifest).delivery_sequence == 0 + + def test_receipt_digest_mismatch_keeps_delivery_uncertain_and_blocks_next_edge( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/test_hosted_runner_adapter.py b/tests/test_hosted_runner_adapter.py new file mode 100644 index 00000000..40146033 --- /dev/null +++ b/tests/test_hosted_runner_adapter.py @@ -0,0 +1,686 @@ +from __future__ import annotations + +import hashlib +import json +import os +from base64 import b64encode, urlsafe_b64encode +from dataclasses import replace +from datetime import datetime, timezone +from pathlib import Path + +import pytest +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +import openadapt_flow.runner.hosted_adapter as hosted +from openadapt_flow.ir import ParamKind, ParamSpec +from openadapt_flow.runner.hosted_adapter import ( + AdmissionArtifactBytes, + DeliveryAuthority, + HostedDispatch, + HostedRunnerAdapter, + ManagedExecution, + RegisterCapabilities, +) +from openadapt_flow.runner.product_release import ( + DOMAIN, + TARGETS, + ProductReleaseAdmissionArtifact, + ProductReleaseAdmissionError, + ProductReleaseAdmissionPayload, + ProductReleaseSignerTrust, + verify_product_release_admission, +) +from openadapt_flow.runner.protocol import ( + DispatchParamsRef, + RunnerDispatchPayload, + dispatch_binding_sha256, +) +from openadapt_flow.runner.verify import VerifiedDispatch +from openadapt_flow.runtime.durable.authority import REMOTE_DISPATCH_SESSION_ID_ENV +from openadapt_flow.transaction import TransactionOutcome +from tests.test_runner_client_lib import dispatch_payload + +pytest_plugins = ("tests.test_runner_client_lib",) + + +def _release_payload() -> dict[str, object]: + targets = [] + for index, target in enumerate(TARGETS, start=1): + targets.append( + { + "target": target, + "admission_id": f"00000000-0000-4000-8000-{index:012d}", + "admission_sha256": f"{index:x}" * 64, + "release_id": "1.2.3", + "release_artifact_sha256": f"{index + 7:x}" * 64, + "admission_issued_at": "2026-08-25T00:00:00Z", + "admission_expires_at": "2026-08-28T00:00:00Z", + "revoked_at": None, + "artifact_authority_sha256": f"{index + 8:x}" * 64, + "artifact_authority_state": "active", + "artifact_authority_checked_at": "2026-08-25T00:00:00Z", + "artifact_authority_expires_at": "2026-08-28T00:00:00Z", + } + ) + return { + "schema_version": "openadapt.product-release-admission-payload/v1", + "set_id": "00000000-0000-4000-8000-000000000099", + "sequence": 7, + "policy_sha256": "a" * 64, + "issued_at": "2026-08-25T00:00:00Z", + "expires_at": "2026-08-28T00:00:00Z", + "targets": tuple(targets), + } + + +def _release_artifact() -> tuple[ + ProductReleaseAdmissionArtifact, ProductReleaseSignerTrust +]: + private_key = Ed25519PrivateKey.from_private_bytes(bytes(range(1, 33))) + public_key = private_key.public_key().public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw, + ) + payload = ProductReleaseAdmissionPayload.model_validate(_release_payload()) + signature = private_key.sign(DOMAIN + payload.canonical_bytes()) + public_b64 = b64encode(public_key).decode("ascii") + artifact = ProductReleaseAdmissionArtifact.model_validate( + { + "schema_version": "openadapt.product-release-admission-artifact/v1", + "payload": payload, + "payload_sha256": payload.payload_sha256_value(), + "signer": { + "algorithm": "ed25519", + "key_id": ( + "release-admission-ed25519-" + + hashlib.sha256(public_key).hexdigest()[:16] + ), + "public_key": public_b64, + }, + "signature": urlsafe_b64encode(signature).decode("ascii").rstrip("="), + } + ) + return artifact, ProductReleaseSignerTrust( + public_key=public_b64, + status="active", + revoked_at=None, + ) + + +@pytest.mark.parametrize("sequence", [True, "7"]) +def test_product_release_sequence_refuses_scalar_coercion(sequence: object) -> None: + raw = _release_payload() + raw["sequence"] = sequence + with pytest.raises(ValueError): + ProductReleaseAdmissionPayload.model_validate(raw) + + +def test_product_release_refuses_noncanonical_utc() -> None: + raw = _release_payload() + raw["issued_at"] = "2026-08-25T00:00:00+00:00" + with pytest.raises(ValueError, match="canonical UTC"): + ProductReleaseAdmissionPayload.model_validate(raw) + + +def test_product_release_refuses_revoked_signer() -> None: + artifact, trust = _release_artifact() + revoked = trust.model_copy( + update={"status": "revoked", "revoked_at": "2026-08-25T00:00:00Z"} + ) + with pytest.raises(ProductReleaseAdmissionError, match="revoked"): + verify_product_release_admission( + artifact, + trusted_signers={artifact.signer.key_id: revoked}, + newest_sequence=7, + now=datetime(2026, 8, 26, tzinfo=timezone.utc), + ) + + +def _hosted_dispatch(workflow) -> HostedDispatch: + workflow_id = "33333333-3333-4333-8333-333333333333" + version_id = "44444444-4444-4444-8444-444444444444" + payload_raw = dispatch_payload( + workflow, + workflow_id=workflow_id, + bundle={ + "version_id": version_id, + "content_digest": workflow.manifest.content_digest, + "url": "https://invalid.example/never-fetched", + }, + ) + payload = RunnerDispatchPayload.model_validate(payload_raw) + artifact_raw = b"{}" + artifact = AdmissionArtifactBytes( + artifact_bytes_base64=b64encode(artifact_raw).decode("ascii"), + artifact_sha256=hashlib.sha256(artifact_raw).hexdigest(), + ) + return HostedDispatch( + schema_version="openadapt.hosted-runner/v1", + dispatch_id="11111111-1111-4111-8111-111111111111", + dispatch_session_id="12111111-1111-4111-8111-111111111111", + tenant_id="22222222-2222-4222-8222-222222222222", + runner_id="55555555-5555-4555-8555-555555555555", + runner_session_id="66666666-6666-4666-8666-666666666666", + run_id=payload.run_id, + workflow_id=workflow_id, + workflow_version_id=version_id, + idempotency_key="hosted-dispatch-0001", + lease_token="oal_" + "a" * 64, + lease_expires_at="2099-01-01T00:00:00Z", + product_release_admission=artifact, + workflow_admission=artifact, + managed_delivery_authority_url=( + "https://cloud.example/api/internal/managed-delivery-permit" + ), + delivery_authority_token="b" * 64, + payload=payload, + ) + + +def test_hosted_dispatch_accepts_lowercase_v8_run_id(sealed) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + run_id = "018f6c0a-4cce-8f47-8d71-c3d63bf1c001" + payload = dispatch.payload.model_copy( + update={ + "run_id": run_id, + "dispatch_binding_sha256": dispatch_binding_sha256( + run_id, dispatch.payload.authorization + ), + } + ) + + parsed = HostedDispatch.model_validate( + dispatch.model_dump(mode="python") | {"run_id": run_id, "payload": payload} + ) + + assert parsed.run_id == run_id + + +def test_registration_refuses_without_protected_runner_origin( + monkeypatch, tmp_path, config +) -> None: + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite") + monkeypatch.setattr(hosted, "load_runner_config", lambda *_args, **_kwargs: config) + + with pytest.raises(ValueError, match="protected runner host"): + adapter.registration_request( + runner_config=tmp_path / "runner.toml", + name="runner", + platform="linux", + agent_version="1.0.0", + engine_version="1.33.0", + mode="service", + capabilities=RegisterCapabilities( + backends=("linux",), + attended=False, + effects_substrates=("linux",), + ), + ) + + +def _prepared_adapter(monkeypatch, tmp_path, config, workflow, runner): + config = replace(config, host="https://cloud.example") + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite", runner=runner) + dispatch = _hosted_dispatch(workflow) + verified = VerifiedDispatch( + payload=dispatch.payload, + bundle=config.bundles[workflow.manifest.content_digest], + profile_path=config.profiles["default"], + params={"visit_date": "2026-07-01"}, + workflow=workflow, + consequential_steps=1, + effect_covered_consequential_steps=1, + ) + monkeypatch.setattr(hosted, "load_runner_config", lambda _, **__: config) + monkeypatch.setattr(adapter, "_verify_product_release", lambda *_: None) + monkeypatch.setattr(adapter, "_load_evidence_private_key", lambda *_: object()) + monkeypatch.setattr( + adapter, + "_verify_workflow_admission", + lambda *_, **__: ({}, b"runtime:\n durable: false\n"), + ) + monkeypatch.setattr(adapter, "_resolve_params", lambda *_: verified.params) + monkeypatch.setattr( + hosted, + "verify_dispatch", + lambda _payload, staged_config, **_kwargs: replace( + verified, + profile_path=staged_config.profiles[dispatch.payload.deployment_profile_id], + ), + ) + + class Guard: + def __init__(self, *_args, **_kwargs): + pass + + def authorization_binding(self, _workflow): + return {} + + monkeypatch.setattr(hosted, "ProductionQualificationGuard", Guard) + return adapter, dispatch + + +@pytest.mark.parametrize( + ("fault", "execution"), + [ + ( + "backend_response_lost", + RuntimeError("backend response lost after possible actuation"), + ), + ( + "delivery_acknowledgment_response_lost", + RuntimeError("delivery acknowledgment response lost after backend call"), + ), + ( + "receipt_unavailable", + ManagedExecution(returncode=1, report_bytes=None), + ), + ( + "malformed_terminal_report", + ManagedExecution(returncode=0, report_bytes=b"not-json"), + ), + ], + ids=( + "backend-response-lost", + "delivery-acknowledgment-response-lost", + "receipt-unavailable", + "malformed-terminal-report", + ), +) +def test_hosted_uncertain_delivery_fault_never_replays( + monkeypatch, tmp_path, config, sealed, fault, execution +) -> None: + workflow, _ = sealed + calls = 0 + seen_child_env = None + seen_argv = None + + def runner(argv, _run_dir, child_env): + nonlocal calls + nonlocal seen_child_env + nonlocal seen_argv + calls += 1 + seen_child_env = child_env + seen_argv = argv + if isinstance(execution, Exception): + raise execution + return execution + + adapter, dispatch = _prepared_adapter( + monkeypatch, tmp_path, config, workflow, runner + ) + authority = DeliveryAuthority( + dispatch.managed_delivery_authority_url, + dispatch.delivery_authority_token, + ) + first = adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run", + authority=authority, + ) + second = adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run-again", + authority=authority, + ) + + assert first.outcome is TransactionOutcome.RECONCILIATION_REQUIRED + assert first.started is True + assert first.uncertain_delivery is True + assert second.outcome is TransactionOutcome.RECONCILIATION_REQUIRED + assert second.started is True + assert second.uncertain_delivery is True + assert calls == 1 + assert seen_child_env[REMOTE_DISPATCH_SESSION_ID_ENV] == ( + dispatch.dispatch_session_id + ) + profile_path = Path(seen_argv[seen_argv.index("--config") + 1]) + assert profile_path == tmp_path / "run" / "deployment.yaml" + assert profile_path.read_bytes() == b"runtime:\n durable: false\n" + if os.name != "nt": + assert profile_path.stat().st_mode & 0o777 == 0o600 + assert fault in { + "backend_response_lost", + "delivery_acknowledgment_response_lost", + "receipt_unavailable", + "malformed_terminal_report", + } + + +def test_params_reference_resolves_only_from_protected_local_root( + monkeypatch, tmp_path, config, sealed +) -> None: + workflow, _ = sealed + workflow.param_specs = { + "visit_date": ParamSpec( + name="visit_date", + type=ParamKind.DATE, + required=True, + ) + } + dispatch = _hosted_dispatch(workflow) + expected_digest = dispatch.payload.authorization.runtime_inputs_digest + dispatch = dispatch.model_copy( + update={ + "payload": dispatch.payload.model_copy( + update={ + "params": DispatchParamsRef( + ref="records/run.json", + expected_digest=expected_digest, + ) + } + ) + } + ) + root = tmp_path / "params" + nested = root / "records" + nested.mkdir(parents=True, mode=0o700) + root.chmod(0o700) + nested.chmod(0o700) + ref_file = nested / "run.json" + ref_file.write_text(json.dumps({"visit_date": "2026-07-01"}), encoding="utf-8") + ref_file.chmod(0o600) + local_config = replace( + config, + host="https://cloud.example", + params_ref_root=root, + ) + monkeypatch.setattr(hosted.Workflow, "load", lambda *_: workflow) + + adapter = HostedRunnerAdapter(tmp_path / "resolver-ledger.sqlite") + assert adapter._resolve_params(dispatch, local_config) == { + "visit_date": "2026-07-01" + } + + traversing = dispatch.model_copy( + update={ + "payload": dispatch.payload.model_copy( + update={ + "params": DispatchParamsRef( + ref="../outside.json", + expected_digest=expected_digest, + ) + } + ) + } + ) + with pytest.raises(ValueError, match="safe local path"): + adapter._resolve_params(traversing, local_config) + + +@pytest.mark.skipif(os.name == "nt", reason="POSIX symlink contract") +def test_params_reference_refuses_symlink( + monkeypatch, tmp_path, config, sealed +) -> None: + workflow, _ = sealed + workflow.param_specs = { + "visit_date": ParamSpec(name="visit_date", type=ParamKind.DATE) + } + dispatch = _hosted_dispatch(workflow) + expected_digest = dispatch.payload.authorization.runtime_inputs_digest + root = tmp_path / "params" + root.mkdir(mode=0o700) + target = tmp_path / "target.json" + target.write_text(json.dumps({"visit_date": "2026-07-01"}), encoding="utf-8") + target.chmod(0o600) + (root / "run.json").symlink_to(target) + dispatch = dispatch.model_copy( + update={ + "payload": dispatch.payload.model_copy( + update={ + "params": DispatchParamsRef( + ref="run.json", + expected_digest=expected_digest, + ) + } + ) + } + ) + monkeypatch.setattr(hosted.Workflow, "load", lambda *_: workflow) + adapter = HostedRunnerAdapter(tmp_path / "resolver-ledger.sqlite") + + with pytest.raises(ValueError, match="private regular file"): + adapter._resolve_params(dispatch, replace(config, params_ref_root=root)) + + +def test_params_reference_digest_mismatch_refuses_before_managed_runner( + monkeypatch, tmp_path, config, sealed +) -> None: + workflow, _ = sealed + workflow.param_specs = { + "visit_date": ParamSpec( + name="visit_date", + type=ParamKind.DATE, + required=True, + ) + } + dispatch = _hosted_dispatch(workflow) + root = tmp_path / "params" + root.mkdir(mode=0o700) + ref_file = root / "run.json" + ref_file.write_text(json.dumps({"visit_date": "2026-07-01"}), encoding="utf-8") + ref_file.chmod(0o600) + dispatch = dispatch.model_copy( + update={ + "payload": dispatch.payload.model_copy( + update={ + "params": DispatchParamsRef( + ref="run.json", + expected_digest="f" * 64, + ) + } + ) + } + ) + local_config = replace( + config, + host="https://cloud.example", + params_ref_root=root, + ) + calls = 0 + + def runner(*_args): + nonlocal calls + calls += 1 + raise AssertionError("managed runner must not start") + + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite", runner=runner) + monkeypatch.setattr( + hosted, "load_runner_config", lambda *_args, **_kwargs: local_config + ) + monkeypatch.setattr(adapter, "_verify_product_release", lambda *_: None) + monkeypatch.setattr(adapter, "_load_evidence_private_key", lambda *_: object()) + monkeypatch.setattr( + adapter, + "_verify_workflow_admission", + lambda *_, **__: ({}, b"runtime:\n durable: false\n"), + ) + monkeypatch.setattr(hosted.Workflow, "load", lambda *_: workflow) + authority = DeliveryAuthority( + dispatch.managed_delivery_authority_url, + dispatch.delivery_authority_token, + ) + + result = adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run", + authority=authority, + ) + + assert result.outcome == "REJECTED_POLICY" + assert result.code == "runtime_inputs_mismatch" + assert result.started is False + assert result.uncertain_delivery is False + assert calls == 0 + + +@pytest.mark.parametrize("manifest_kind", ["missing", "malformed", "public", "symlink"]) +def test_untrusted_runner_manifest_refuses_before_managed_runner( + tmp_path, sealed, manifest_kind +) -> None: + if manifest_kind == "symlink" and os.name == "nt": + pytest.skip("POSIX symlink contract") + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + manifest = tmp_path / "runner.toml" + if manifest_kind == "malformed": + manifest.write_text("[runner\n", encoding="utf-8") + manifest.chmod(0o600) + elif manifest_kind == "public": + manifest.write_text("[runner]\nname = 'runner'\n", encoding="utf-8") + manifest.chmod(0o644) + elif manifest_kind == "symlink": + target = tmp_path / "target.toml" + target.write_text("[runner]\nname = 'runner'\n", encoding="utf-8") + target.chmod(0o600) + manifest.symlink_to(target) + calls = 0 + + def runner(*_args): + nonlocal calls + calls += 1 + raise AssertionError("managed runner must not start") + + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite", runner=runner) + authority = DeliveryAuthority( + dispatch.managed_delivery_authority_url, + dispatch.delivery_authority_token, + ) + + result = adapter.execute( + dispatch, + runner_config=manifest, + run_dir=tmp_path / "run", + authority=authority, + ) + + assert result.outcome == "REJECTED_POLICY" + assert result.code == "hosted_admission_refused" + assert result.detail.startswith("prestart_") + assert result.started is False + assert result.uncertain_delivery is False + assert calls == 0 + + +@pytest.mark.parametrize( + "runner_host", + [ + None, + "http://cloud.example", + "https://cloud.example/", + "https://different.example", + ], +) +def test_protected_runner_host_binds_delivery_authority_origin( + monkeypatch, tmp_path, config, sealed, runner_host +) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + calls = 0 + + def runner(*_args): + nonlocal calls + calls += 1 + raise AssertionError("managed runner must not start") + + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite", runner=runner) + local_config = replace(config, host=runner_host) + monkeypatch.setattr( + hosted, "load_runner_config", lambda *_args, **_kwargs: local_config + ) + authority = DeliveryAuthority( + dispatch.managed_delivery_authority_url, + dispatch.delivery_authority_token, + ) + + result = adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run", + authority=authority, + ) + + assert result.outcome == "REJECTED_POLICY" + assert result.started is False + assert result.detail == "prestart_ValueError" + assert calls == 0 + + +def test_protected_profile_mutation_refuses_before_managed_runner( + monkeypatch, tmp_path, config, sealed +) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + config = replace(config, host="https://cloud.example") + profile_path = config.profiles["default"] + profile_path.chmod(0o600) + calls = 0 + + def runner(*_args): + nonlocal calls + calls += 1 + raise AssertionError("managed runner must not start") + + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite", runner=runner) + monkeypatch.setattr(hosted, "load_runner_config", lambda *_args, **_kwargs: config) + monkeypatch.setattr(adapter, "_verify_product_release", lambda *_: None) + monkeypatch.setattr(adapter, "_load_evidence_private_key", lambda *_: object()) + + def verify_profile(*_args, **_kwargs): + raw = adapter._read_private_bytes( + profile_path, + maximum_bytes=1024 * 1024, + label="hosted deployment profile", + ) + return {}, raw + + monkeypatch.setattr(adapter, "_verify_workflow_admission", verify_profile) + real_read = os.read + changed = False + + def mutating_read(descriptor, count): + nonlocal changed + chunk = real_read(descriptor, count) + if not changed: + changed = True + profile_path.write_bytes(chunk + b"# changed\n") + profile_path.chmod(0o600) + return chunk + + monkeypatch.setattr(hosted.os, "read", mutating_read) + authority = DeliveryAuthority( + dispatch.managed_delivery_authority_url, + dispatch.delivery_authority_token, + ) + + result = adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run", + authority=authority, + ) + + assert result.outcome == "REJECTED_POLICY" + assert result.started is False + assert result.detail == "prestart_ValueError" + assert calls == 0 + + +def test_parsed_refusal_callback_contains_closed_terminal(tmp_path, sealed) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite") + refusal = adapter._refusal(dispatch, "hosted_admission_refused", "refused") + + callback = adapter.callback_request(dispatch, refusal) + + terminal = callback.events[-1] + assert terminal["schema_version"] == "openadapt.hosted-runner-terminal/v1" + assert terminal["outcome"] == "REJECTED_POLICY" + assert terminal["started"] is False + assert terminal["uncertain_delivery"] is False diff --git a/tests/test_runner_client_lib.py b/tests/test_runner_client_lib.py index 507100d4..89e1786e 100644 --- a/tests/test_runner_client_lib.py +++ b/tests/test_runner_client_lib.py @@ -333,7 +333,7 @@ def test_dispatch_binding_known_vector(self): dispatch_binding_sha256( "11111111-1111-4111-8111-111111111111", authorization ) - == "sha256:efd01f7c8c56a0df02200d684a5ab6104e47ec769090b2d76eb090624cdcc272" + == "sha256:367411c4ff350c05d6dad465db3dd1f57e8d47d620d3c0f16b70adec0857047e" ) def test_dispatch_binding_refuses_changed_run_or_authorization(self, sealed): From 40f03bb6cb0ce0d1886cd8d7cd7efc1e0836cf91 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Wed, 26 Aug 2026 14:58:17 -0400 Subject: [PATCH 2/9] fix: type hosted runner release bindings --- openadapt_flow/runner/config.py | 12 +++++++++--- openadapt_flow/runner/hosted_adapter.py | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/openadapt_flow/runner/config.py b/openadapt_flow/runner/config.py index d752542c..5fcb26c9 100644 --- a/openadapt_flow/runner/config.py +++ b/openadapt_flow/runner/config.py @@ -45,7 +45,7 @@ import stat from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Optional +from typing import Any, Literal, Optional from openadapt_flow.hosted import HostedError from openadapt_flow.private_file import ( @@ -59,6 +59,8 @@ ) _SAFE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,199}$") +LocalReleaseTarget = Literal["flow", "desktop", "capture"] + def _load_manifest_toml(path: Path, *, protected: bool = False) -> dict[str, Any]: """Full-TOML parse (the manifest uses ``[[bundles]]`` array tables, which @@ -203,7 +205,7 @@ class BusinessDecisionServiceConfig: class LocalRuntimeRelease: """One independently installed target release used during enrollment.""" - target: str + target: LocalReleaseTarget admission_id: str admission_sha256: str release_version: str @@ -372,7 +374,11 @@ def load_runner_config( if not isinstance(local_release_tbl, dict): raise RunnerConfigError("[local_runtime_release] must be a table") local_runtime_release: list[LocalRuntimeRelease] = [] - expected_release_targets = ("flow", "desktop", "capture") + expected_release_targets: tuple[LocalReleaseTarget, ...] = ( + "flow", + "desktop", + "capture", + ) for target in expected_release_targets: entry = local_release_tbl.get(target) if entry is None: diff --git a/openadapt_flow/runner/hosted_adapter.py b/openadapt_flow/runner/hosted_adapter.py index 45c16fb7..361482dd 100644 --- a/openadapt_flow/runner/hosted_adapter.py +++ b/openadapt_flow/runner/hosted_adapter.py @@ -468,10 +468,10 @@ def registration_request( *, runner_config: Path, name: str, - platform: str, + platform: Literal["windows", "macos", "linux"], agent_version: str, engine_version: str, - mode: str, + mode: Literal["attended", "service"], capabilities: RegisterCapabilities | Mapping[str, object], ) -> RegisterRequest: config = load_runner_config(runner_config, protected=True) From bf8b0a8192af2286e5eb4ee17fcda01200ce7684 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Wed, 26 Aug 2026 17:10:22 -0400 Subject: [PATCH 3/9] feat: complete hosted runner target state --- openadapt_flow/__init__.py | 2 +- openadapt_flow/__main__.py | 35 +- openadapt_flow/action_evidence.py | 10 +- openadapt_flow/compiler/induction.py | 6 +- openadapt_flow/console/halt_detail.py | 5 +- openadapt_flow/deployment.py | 6 +- openadapt_flow/execution_profiles.py | 67 ++- openadapt_flow/ir.py | 7 +- openadapt_flow/learning/halt_loop.py | 3 +- openadapt_flow/learning/teach.py | 3 +- openadapt_flow/qualification.py | 27 +- openadapt_flow/runner/__init__.py | 4 + openadapt_flow/runner/hosted_adapter.py | 500 ++++++++++++++-- openadapt_flow/runner/inputs.py | 38 +- openadapt_flow/runner/protocol.py | 38 +- openadapt_flow/runner/verify.py | 10 +- openadapt_flow/runtime/authorization.py | 174 +++++- openadapt_flow/runtime/durable/attended.py | 30 +- .../runtime/durable/business_decision.py | 7 +- openadapt_flow/runtime/durable/checkpoint.py | 11 +- openadapt_flow/runtime/durable/controller.py | 11 +- .../runtime/durable/program_checkpoint.py | 8 +- openadapt_flow/runtime/durable/resume.py | 13 +- openadapt_flow/runtime/effects/adapter.py | 2 +- openadapt_flow/runtime/effects/effect.py | 34 +- openadapt_flow/runtime/program_predicates.py | 10 +- openadapt_flow/runtime/replayer.py | 153 +++-- openadapt_flow/visualize/builder.py | 7 +- pyproject.toml | 2 +- tests/test_effect_kit_config.py | 19 + tests/test_execution_profiles.py | 49 +- tests/test_governed_authorization.py | 56 ++ tests/test_hosted_runner_adapter.py | 549 +++++++++++++++++- tests/test_program_ir_phase1.py | 23 + tests/test_runner_client_lib.py | 38 ++ uv.lock | 2 +- 36 files changed, 1702 insertions(+), 257 deletions(-) diff --git a/openadapt_flow/__init__.py b/openadapt_flow/__init__.py index 5b7c47ca..5a782114 100644 --- a/openadapt_flow/__init__.py +++ b/openadapt_flow/__init__.py @@ -1,6 +1,6 @@ """openadapt-flow: record once, compile, replay deterministically, heal on drift.""" -__version__ = "1.33.0" +__version__ = "1.34.0" from openadapt_flow.ir import ( # noqa: F401 ActionKind, diff --git a/openadapt_flow/__main__.py b/openadapt_flow/__main__.py index b603836e..70a0c6a9 100644 --- a/openadapt_flow/__main__.py +++ b/openadapt_flow/__main__.py @@ -65,13 +65,23 @@ import sys from contextlib import contextmanager from pathlib import Path -from typing import TYPE_CHECKING, Any, Iterator, Literal, Optional, Sequence, cast +from typing import ( + TYPE_CHECKING, + Any, + Iterator, + Literal, + Mapping, + Optional, + Sequence, + cast, +) from urllib.parse import urlsplit from uuid import UUID if TYPE_CHECKING: # pragma: no cover from openadapt_flow.backend import Backend from openadapt_flow.ir import ExecutionTargetKind, RunReport + from openadapt_flow.runtime.authorization import RuntimeParamScalar from openadapt_flow.tutorial import BreakItResult _VIEWPORT = {"width": 1280, "height": 800} @@ -258,7 +268,7 @@ def _resolve_record_capture_window( def _replay_params( pairs: Sequence[str] | None, params_file: str | None = None, -) -> dict[str, str]: +) -> dict[str, "RuntimeParamScalar"]: """Load replay bindings without requiring sensitive values in argv. ``--params-file`` is intended for managed runners: the file can be staged @@ -267,7 +277,9 @@ def _replay_params( """ import json - params: dict[str, str] = {} + from openadapt_flow.runtime.authorization import is_runtime_param_scalar + + params: dict[str, RuntimeParamScalar] = {} if params_file: path = Path(params_file) try: @@ -281,11 +293,9 @@ def _replay_params( for key, value in raw.items(): if not isinstance(key, str) or not key: raise SystemExit("--params-file keys must be non-empty strings") - if not isinstance(value, (str, int, float, bool)) or isinstance( - value, (dict, list) - ): + if not is_runtime_param_scalar(value): raise SystemExit(f"--params-file value for {key!r} must be a scalar") - params[key] = str(value) + params[key] = value params.update(_parse_params(pairs)) return params @@ -400,7 +410,10 @@ def _deployment_sections(args: argparse.Namespace): return cfg, effects, actuation -def _deployment_runtime(args: argparse.Namespace, params: dict[str, str] | None = None): +def _deployment_runtime( + args: argparse.Namespace, + params: Mapping[str, "RuntimeParamScalar"] | None = None, +): """Resolve the deployment wiring for a replay/run from ``--config`` + flags. Returns ``(cfg, effect_verifier, api_actuator, durable, allow_egress)``. @@ -416,10 +429,14 @@ def _deployment_runtime(args: argparse.Namespace, params: dict[str, str] | None ignores it. """ from openadapt_flow.deployment import build_api_actuator, build_effect_verifier + from openadapt_flow.runtime.authorization import runtime_params_for_gui cfg, effects, actuation = _deployment_sections(args) try: - effect_verifier = build_effect_verifier(effects, params=params) + effect_verifier = build_effect_verifier( + effects, + params=runtime_params_for_gui(params or {}), + ) api_actuator = build_api_actuator(actuation) except ValueError as e: raise SystemExit(str(e)) diff --git a/openadapt_flow/action_evidence.py b/openadapt_flow/action_evidence.py index 2ef604e1..97987687 100644 --- a/openadapt_flow/action_evidence.py +++ b/openadapt_flow/action_evidence.py @@ -13,6 +13,7 @@ from typing import Any, Optional from openadapt_flow.ir import ActionKind, Step +from openadapt_flow.runtime.authorization import RuntimeParamScalar, runtime_param_text AUTOMATED_GUI_ACTUATIONS = frozenset( {"uia", "dom", "guarded_coordinate", "guarded_keyboard", "remote_guarded"} @@ -177,7 +178,7 @@ def _delivery_receipt_error( step: Step, result: Any, *, - params: Mapping[str, str], + params: Mapping[str, RuntimeParamScalar], ) -> Optional[str]: receipt = result.delivery_receipt if receipt is None: @@ -226,7 +227,10 @@ def _delivery_receipt_error( return "non-drag delivery receipt contains a destination fingerprint" if step.action is ActionKind.SELECT_OPTION: - selected = params.get(step.param) if step.param is not None else step.text + selected_value = params.get(step.param) if step.param is not None else step.text + selected = ( + runtime_param_text(selected_value) if selected_value is not None else None + ) if selected is None or step.selection_commit_key is None: return "selection delivery receipt lacks its compiled input contract" if ( @@ -287,7 +291,7 @@ def action_evidence_error( step: Step, result: Any, *, - params: Mapping[str, str] | None = None, + params: Mapping[str, RuntimeParamScalar] | None = None, identity_required: bool = False, strict_production: bool = True, ) -> Optional[str]: diff --git a/openadapt_flow/compiler/induction.py b/openadapt_flow/compiler/induction.py index 54a32a10..84904730 100644 --- a/openadapt_flow/compiler/induction.py +++ b/openadapt_flow/compiler/induction.py @@ -110,6 +110,7 @@ Transition, Workflow, ) +from openadapt_flow.runtime.authorization import runtime_param_text TraceInput = Union[Workflow, str, Path] @@ -717,7 +718,10 @@ def induce_program( program=program, subflows=subflows, param_specs=param_specs, - params={k: (v.example or "") for k, v in param_specs.items()}, + params={ + key: runtime_param_text(spec.example) if spec.example is not None else "" + for key, spec in param_specs.items() + }, data_sources=data_sources, ) result.program = program diff --git a/openadapt_flow/console/halt_detail.py b/openadapt_flow/console/halt_detail.py index d91adcfc..c171882e 100644 --- a/openadapt_flow/console/halt_detail.py +++ b/openadapt_flow/console/halt_detail.py @@ -53,6 +53,7 @@ from openadapt_flow.console import data from openadapt_flow.ir import ActionKind, Anchor, Rung, Step, Workflow from openadapt_flow.runtime import identity as _id +from openadapt_flow.runtime.authorization import runtime_params_for_gui from openadapt_flow.runtime.durable.checkpoint import CheckpointStore, PendingEscalation #: The resolution ladder in strongest-first order, taken from the engine's own @@ -444,9 +445,9 @@ def halt_detail( anchor = step.anchor if step is not None else None params: dict[str, str] = {} if report is not None and getattr(report, "params", None): - params = dict(report.params) + params = runtime_params_for_gui(report.params) elif pending is not None: - params = dict(pending.params) + params = runtime_params_for_gui(pending.params) role, label = _safe_target_label(step, params) resolved_rung = None diff --git a/openadapt_flow/deployment.py b/openadapt_flow/deployment.py index 09449741..7359e9bf 100644 --- a/openadapt_flow/deployment.py +++ b/openadapt_flow/deployment.py @@ -908,7 +908,7 @@ def build_replayer( def _resolve_config_exprs( section: str, exprs: Mapping[str, ValueExpr], - params: Optional[Mapping[str, str]], + params: Optional[Mapping[str, object]], ) -> dict[str, str]: """Resolve a config's ``ValueExpr`` mapping against the run's params. @@ -945,7 +945,7 @@ def _require_env(name: str, what: str) -> str: def build_effect_verifier( - cfg: EffectsConfig, params: Optional[Mapping[str, str]] = None + cfg: EffectsConfig, params: Optional[Mapping[str, object]] = None ) -> Optional[Any]: """Construct the configured ``EffectVerifier`` (or None for ``kind: none``). @@ -1073,7 +1073,7 @@ def sanitized(value: Any, *, key: str = "") -> Any: def _build_effect_verifier_unredacted( - cfg: EffectsConfig, params: Optional[Mapping[str, str]] = None + cfg: EffectsConfig, params: Optional[Mapping[str, object]] = None ) -> Optional[Any]: """The per-kind construction behind :func:`build_effect_verifier`.""" kind = (cfg.kind or "none").strip().lower() diff --git a/openadapt_flow/execution_profiles.py b/openadapt_flow/execution_profiles.py index eff5bc8c..4f344045 100644 --- a/openadapt_flow/execution_profiles.py +++ b/openadapt_flow/execution_profiles.py @@ -26,6 +26,11 @@ action_evidence_error, ) from openadapt_flow.decision_delivery import DecisionDeliveryTier +from openadapt_flow.runtime.authorization import ( + RuntimeParamScalar, + runtime_param_text, + runtime_params_for_gui, +) from openadapt_flow.verification import VerificationTier if TYPE_CHECKING: @@ -289,7 +294,7 @@ def _api_identity_evidence_is_exact( workflow: Workflow, step: Any, check: Any, - scoped_params: Mapping[str, str], + scoped_params: Mapping[str, RuntimeParamScalar], effects: list[Any], ) -> bool: """Return whether an API result matches its exact identity binding. @@ -303,6 +308,10 @@ def _api_identity_evidence_is_exact( binding = step.api_binding if binding is None or not binding.identity or check is None: return False + try: + text_params = runtime_params_for_gui(scoped_params) + except ValueError: + return False project = workflow.qualification policy = project.identity_policies.get(step.id) if project is not None else None if policy is not None: @@ -316,7 +325,7 @@ def _api_identity_evidence_is_exact( check=check, step=step, actuation_path="api", - runtime_params=scoped_params, + runtime_params=text_params, recorded_params=workflow.params, ) is not None @@ -353,7 +362,7 @@ def _api_identity_evidence_is_exact( return False for identity in binding.identity: - if not scoped_params.get(identity.param): + if identity.param not in text_params or text_params[identity.param] == "": return False effect_path = tuple(identity.effect_field.split(".")) if not any( @@ -413,7 +422,7 @@ def _program_action_trace( workflow: Workflow, visited_states: list[str], *, - runtime_params: Mapping[str, str] | None = None, + runtime_params: Mapping[str, str | bool | int | float] | None = None, runtime_worklists: Mapping[str, list[dict[str, str]]] | None = None, transition_evidence: list[Any] | None = None, exception_evidence: list[Any] | None = None, @@ -498,7 +507,7 @@ def _rows(relation: str) -> list[dict[str, str]] | None: return None if declared is None else list(declared.rows) def _reported_guard_value( - predicate: Any, current_params: Mapping[str, str] + predicate: Any, current_params: Mapping[str, str | bool | int | float] ) -> bool | None: """Recompute guards whose inputs are retained in the run report. @@ -511,9 +520,13 @@ def _reported_guard_value( kind = predicate.kind if kind is PredicateKind.PARAM_EQUALS: - return predicate.param is not None and str( - current_params.get(predicate.param) - ) == str(predicate.value) + return ( + predicate.param is not None + and predicate.value is not None + and predicate.param in current_params + and runtime_param_text(current_params[predicate.param]) + == predicate.value + ) if kind is PredicateKind.AND: values = [ _reported_guard_value(item, current_params) @@ -618,7 +631,7 @@ def _validated_evidence_target( graph_id: str, state: Any, scope: tuple[Any, ...], - current_params: Mapping[str, str], + current_params: Mapping[str, str | bool | int | float], ) -> str | None: nonlocal evaluator_contract_sha256 group = _matching_evidence_group( @@ -726,7 +739,7 @@ def _validated_evidence_target( recomputed_visual = evaluate_program_predicate( transition.guard, frame, - current_params, + runtime_params_for_gui(current_params), vision=transition_predicate_vision, viewport=item.observed_viewport, asset_loader=retained_assets.get, @@ -745,7 +758,7 @@ def _validated_attended_target( graph_id: str, state: Any, scope: tuple[Any, ...], - current_params: Mapping[str, str], + current_params: Mapping[str, str | bool | int | float], ) -> tuple[bool, str | None, str | None]: nonlocal attended_evidence_cursor, expected_evidence_decision_index evidence = attended_transition_evidence or [] @@ -869,7 +882,7 @@ def _validated_business_decision( graph_id: str, state: Any, scope: tuple[Any, ...], - current_params: dict[str, str], + current_params: dict[str, str | bool | int | float], ) -> str: nonlocal business_evidence_cursor, expected_evidence_decision_index evidence = business_decision_evidence or [] @@ -1013,7 +1026,7 @@ def _validated_exception_target( def _selected_transition_target( state: Any, - current_params: dict[str, str], + current_params: Mapping[str, str | bool | int | float], *, graph_id: str, scope: tuple[Any, ...], @@ -1064,7 +1077,7 @@ def _next_state( state: Any, graph: Any, occurrence_index: int | None, - current_params: dict[str, str], + current_params: Mapping[str, str | bool | int | float], *, graph_id: str, scope: tuple[Any, ...], @@ -1219,7 +1232,7 @@ def _consume_graph( scope: tuple[Any, ...], *, depth: int, - current_params: dict[str, str], + current_params: dict[str, str | bool | int | float], ) -> None: nonlocal cursor, halted_at_requested_action if depth > 64: @@ -1677,7 +1690,7 @@ def classify_execution_outcome( assert minimum is not None from openadapt_flow.ir import ActionKind - def _scoped_params(result: Any) -> dict[str, str] | None: + def _scoped_params(result: Any) -> dict[str, str | bool | int | float] | None: if workflow.program is None and result.program_scope: return None scoped = dict(report.params) @@ -1697,16 +1710,20 @@ def _scoped_params(result: Any) -> dict[str, str] | None: return scoped def _reported_parameter_predicate_value( - predicate: Any, current_params: Mapping[str, str] + predicate: Any, current_params: Mapping[str, str | bool | int | float] ) -> bool | None: """Recompute a guard only when all of its inputs are report-bound.""" from openadapt_flow.ir import PredicateKind if predicate.kind is PredicateKind.PARAM_EQUALS: - return predicate.param is not None and str( - current_params.get(predicate.param) - ) == str(predicate.value) + return ( + predicate.param is not None + and predicate.value is not None + and predicate.param in current_params + and runtime_param_text(current_params[predicate.param]) + == predicate.value + ) if predicate.kind is PredicateKind.AND: values = [ _reported_parameter_predicate_value(item, current_params) @@ -1802,7 +1819,7 @@ def _reported_parameter_predicate_value( check=result.identity, step=step, actuation_path=("api" if result.actuation == "api" else "gui"), - runtime_params=scoped_params, + runtime_params=runtime_params_for_gui(scoped_params), recorded_params=workflow.params, evidence_root=transition_evidence_root, recorded_asset_sha256=recorded_asset_sha256, @@ -1928,7 +1945,7 @@ def _reported_parameter_predicate_value( try: expected_hashes = Counter( effect.resolved_contract_hash( - scoped_params, + runtime_params_for_gui(scoped_params), opaque_param_sha256=opaque, ) for effect in effects @@ -1976,7 +1993,7 @@ def _reported_parameter_predicate_value( ) try: effect_hash = effect.resolved_contract_hash( - scoped_params, + runtime_params_for_gui(scoped_params), opaque_param_sha256=opaque, ) except ValueError: @@ -2163,7 +2180,7 @@ def build_outcome_envelope( envelope_requirements = () effect_requirements_valid = False - def _scoped_params(result: Any) -> dict[str, str] | None: + def _scoped_params(result: Any) -> dict[str, str | bool | int | float] | None: if workflow.program is None and result.program_scope: return None scoped = dict(report.params) @@ -2279,7 +2296,7 @@ def _scoped_params(result: Any) -> dict[str, str] | None: "qualified effect contract does not match" ) effect_hash = effect.resolved_contract_hash( - scoped_params, + runtime_params_for_gui(scoped_params), opaque_param_sha256=opaque, ) expected_hashes.append(effect_hash) diff --git a/openadapt_flow/ir.py b/openadapt_flow/ir.py index b54bf8d6..b6a8ffb2 100644 --- a/openadapt_flow/ir.py +++ b/openadapt_flow/ir.py @@ -29,7 +29,7 @@ from datetime import datetime, timezone from enum import Enum from pathlib import Path, PurePosixPath -from typing import TYPE_CHECKING, Any, Final, Iterator, Literal, Optional +from typing import TYPE_CHECKING, Any, Final, Iterator, Literal, Optional, Union from pydantic import ( BaseModel, @@ -637,6 +637,7 @@ class ParamKind(str, Enum): DATE = "date" ENUM = "enum" NUMBER = "number" + BOOLEAN = "boolean" ENTITY_REF = "entity_ref" @@ -651,7 +652,7 @@ class ParamSpec(BaseModel): name: str type: ParamKind = ParamKind.STRING - example: Optional[str] = Field( + example: Optional[Union[str, bool, int, float]] = Field( default=None, description="Recorded demo value; also the replay default when the " "caller supplies no value for this parameter.", @@ -3634,7 +3635,7 @@ class RunReport(BaseModel): ) required_identity_step_ids: list[str] = Field(default_factory=list) approved_unverified_effect_step_ids: list[str] = Field(default_factory=list) - params: dict[str, str] = Field(default_factory=dict) + params: dict[str, Union[str, bool, int, float]] = Field(default_factory=dict) results: list[StepResult] = Field(default_factory=list) success: bool = False # Workflow-program IR, Phase 2: the outcome of the terminal state the graph diff --git a/openadapt_flow/learning/halt_loop.py b/openadapt_flow/learning/halt_loop.py index 72df1e5c..65997069 100644 --- a/openadapt_flow/learning/halt_loop.py +++ b/openadapt_flow/learning/halt_loop.py @@ -47,6 +47,7 @@ learn_from_traces, ) from openadapt_flow.learning.trace import ExecutionTrace, TraceStep +from openadapt_flow.runtime.authorization import runtime_params_for_gui from openadapt_flow.runtime.healing.governance import RegressionGate @@ -78,7 +79,7 @@ def execution_trace_from_halt( outcome="failure", steps=steps, facts=facts, - params=dict(params or report.params), + params=runtime_params_for_gui(params or report.params), failure_reason=halt.reason, ) diff --git a/openadapt_flow/learning/teach.py b/openadapt_flow/learning/teach.py index 66a74577..abe2d538 100644 --- a/openadapt_flow/learning/teach.py +++ b/openadapt_flow/learning/teach.py @@ -65,6 +65,7 @@ class the halt->learn loop was built for (splice a guarded, reversible dismiss from openadapt_flow.learning.loop import Inducer, LearnOutcome from openadapt_flow.learning.synth_stream import StructuralDiffInducer from openadapt_flow.learning.trace import ExecutionTrace, TraceStep +from openadapt_flow.runtime.authorization import runtime_params_for_gui class TeachError(Exception): @@ -245,7 +246,7 @@ def _correction_from_spec( resolution_steps=resolution_steps, tail_intents=tail, trace_id=f"{report.workflow_name}-correction", - params=spec.params or dict(report.params), + params=spec.params or runtime_params_for_gui(report.params), ) return correction, _baseline_success(program, report, tail) diff --git a/openadapt_flow/qualification.py b/openadapt_flow/qualification.py index 95e68266..ef5791f7 100644 --- a/openadapt_flow/qualification.py +++ b/openadapt_flow/qualification.py @@ -2556,7 +2556,11 @@ def _case_run_report_integrity_error( QualificationRefusalCode.CASE_ATTESTATION_INVALID, "case-input bytes do not match the signed case-result binding", ) - from openadapt_flow.runtime.authorization import parse_runtime_inputs_bytes + from openadapt_flow.runtime.authorization import ( + parse_runtime_inputs_bytes, + runtime_param_text, + runtime_params_for_gui, + ) try: case_params, case_worklists = parse_runtime_inputs_bytes( @@ -2569,7 +2573,9 @@ def _case_run_report_integrity_error( "case input is not a valid canonical governed-input artifact", ) - def scoped_case_params(item: Any) -> Optional[dict[str, str]]: + def scoped_case_params( + item: Any, + ) -> Optional[dict[str, str | bool | int | float]]: if workflow.program is None: return dict(case_params) if not item.program_scope else None if not item.program_scope or item.program_scope[0].graph_id != "__program__": @@ -3155,9 +3161,10 @@ def delivery_receipt_error(item: Any, step: "Step") -> Optional[str]: ) if selected_value is None or step.selection_commit_key is None: return "selection delivery receipt lacks its compiled input contract" + selected_text = runtime_param_text(selected_value) if ( receipt.selection_value_sha256 - != hashlib.sha256(selected_value.encode("utf-8")).hexdigest() + != hashlib.sha256(selected_text.encode("utf-8")).hexdigest() or receipt.selection_commit_key != step.selection_commit_key ): return "selection delivery receipt differs from the compiled input" @@ -3238,7 +3245,7 @@ def identity_evidence_error( check=item.identity, step=step, actuation_path=actuation_path, - runtime_params=scoped, + runtime_params=runtime_params_for_gui(scoped), recorded_params=workflow.params, evidence_root=run_evidence_root, recorded_asset_sha256=recorded_asset_sha256, @@ -3248,7 +3255,7 @@ def identity_evidence_error( check=item.identity, step=step, actuation_path=actuation_path, - runtime_params=scoped, + runtime_params=runtime_params_for_gui(scoped), recorded_params=workflow.params, evidence_root=run_evidence_root, recorded_asset_sha256=recorded_asset_sha256, @@ -3277,7 +3284,7 @@ def resolved_effect_contracts( ( index, effect.resolved_contract_hash( - scoped, + runtime_params_for_gui(scoped), opaque_param_sha256={"__run_id__": result.run_id_sha256 or ""}, ), effect_policies.get((step.id, actuation_path, index)), @@ -3913,7 +3920,7 @@ def result_has_sufficient_effect_evidence(item: Any) -> bool: ( index, effect.resolved_contract_hash( - resolved_params, + runtime_params_for_gui(resolved_params), opaque_param_sha256={ "__run_id__": result.run_id_sha256 or "" }, @@ -4006,7 +4013,11 @@ def result_has_sufficient_effect_evidence(item: Any) -> bool: check=item.identity, step=step, actuation_path=actuation_path, - runtime_params=scoped_case_params(item), + runtime_params=( + runtime_params_for_gui(scoped) + if (scoped := scoped_case_params(item)) is not None + else None + ), recorded_params=workflow.params, evidence_root=run_evidence_root, recorded_asset_sha256=( diff --git a/openadapt_flow/runner/__init__.py b/openadapt_flow/runner/__init__.py index dd641fd5..3d975663 100644 --- a/openadapt_flow/runner/__init__.py +++ b/openadapt_flow/runner/__init__.py @@ -48,6 +48,7 @@ write_managed_dispatch_envelope, ) from openadapt_flow.runner.hosted_adapter import ( + RUNNER_RENEWAL_HEADER, CallbackRequest, CallbackResponse, DeliveryAuthority, @@ -61,6 +62,7 @@ RegisterCapabilities, RegisterRequest, RegisterResponse, + registration_renewal_headers, ) from openadapt_flow.runner.lease import ( CompletionDisposition, @@ -108,6 +110,7 @@ "Refusal", "RefusalCode", "PollRequest", + "RUNNER_RENEWAL_HEADER", "RegisterCapabilities", "RegisterRequest", "RegisterResponse", @@ -126,6 +129,7 @@ "map_control_verb", "read_managed_dispatch_envelope", "parse_dispatch", + "registration_renewal_headers", "server_reclaim_outcome", "verify_dispatch", "write_managed_dispatch_envelope", diff --git a/openadapt_flow/runner/hosted_adapter.py b/openadapt_flow/runner/hosted_adapter.py index 361482dd..9ee003fb 100644 --- a/openadapt_flow/runner/hosted_adapter.py +++ b/openadapt_flow/runner/hosted_adapter.py @@ -39,6 +39,7 @@ QualificationAdmissionEnvelope, QualificationAdmissionExpected, QualificationSignerRegistry, + canonical_json, contract_sha256, verify_qualification_admission, ) @@ -53,16 +54,31 @@ load_product_release_signer_trust, verify_product_release_admission, ) -from openadapt_flow.runner.protocol import DispatchParamsValues, RunnerDispatchPayload +from openadapt_flow.runner.protocol import ( + DispatchParamsValues, + RunnerDispatchPayload, + validate_runtime_param_name, +) +from openadapt_flow.runner.protocol import ( + dispatch_binding_sha256 as governed_dispatch_binding_sha256, +) from openadapt_flow.runner.verify import Refusal, RefusalCode, verify_dispatch +from openadapt_flow.runtime.authorization import RuntimeParamScalar from openadapt_flow.runtime.durable.authority import ( REMOTE_AUTHORITY_TOKEN_ENV, REMOTE_AUTHORITY_URL_ENV, REMOTE_DISPATCH_SESSION_ID_ENV, + DurableAuthority, ) +from openadapt_flow.runtime.durable.checkpoint import CheckpointStore from openadapt_flow.terminal_verification_v2 import ( + ProductionTerminalVerificationContext, ProductionTerminalVerificationEnvelope, + ProductionTerminalVerificationExpected, + build_production_terminal_verification, evidence_runner_signer_sha256, + prepare_production_terminal_evidence, + verify_production_terminal_verification_from_report, ) from openadapt_flow.transaction import ( DuplicateActuation, @@ -80,6 +96,21 @@ _UTC_SECONDS = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$") _MAX_ARTIFACT_BYTES = 2 * 1024 * 1024 +# The current runner credential is never part of a request model. Desktop may +# project it into this header only for POST /api/runners/register on the exact +# protected runner origin. +RUNNER_RENEWAL_HEADER = "x-openadapt-runner-renewal-token" + + +def registration_renewal_headers(current_runner_token: str | None) -> dict[str, str]: + """Return the one register-only renewal header without retaining it.""" + + if current_runner_token is None or current_runner_token == "": + return {} + if re.fullmatch(_RUNNER_TOKEN, current_runner_token) is None: + raise ValueError("current runner renewal credential is invalid") + return {RUNNER_RENEWAL_HEADER: current_runner_token} + def _utc_seconds(value: str, *, label: str) -> datetime: if _UTC_SECONDS.fullmatch(value) is None: @@ -283,6 +314,31 @@ def _verified_requires_exact_proof(self) -> "HostedTerminalEvent": raise ValueError("VERIFIED requires exact terminal verification") if self.outcome != "VERIFIED" and has_proof: raise ValueError("non-VERIFIED callback cannot carry a success proof") + if has_proof: + assert self.terminal_verification_artifact_bytes_base64 is not None + assert self.terminal_verification_artifact_sha256 is not None + try: + raw = b64decode( + self.terminal_verification_artifact_bytes_base64, + validate=True, + ) + proof = ProductionTerminalVerificationEnvelope.model_validate_json(raw) + except (ValueError, TypeError) as exc: + raise ValueError("terminal verification artifact is invalid") from exc + if ( + len(raw) > _MAX_ARTIFACT_BYTES + or b64encode(raw).decode("ascii") + != self.terminal_verification_artifact_bytes_base64 + or canonical_json(proof) != raw + or proof.artifact_sha256() != self.terminal_verification_artifact_sha256 + ): + raise ValueError("terminal verification artifact binding is invalid") + if ( + proof.payload.run_id != self.run_id + or proof.payload.run_report_sha256 != self.report_sha256 + or proof.payload.run_report_object_sha256 != self.report_sha256 + ): + raise ValueError("terminal verification names a different run report") return self @@ -308,6 +364,14 @@ def _closed_terminal(self) -> "HostedRunResult": TransactionOutcome.VERIFIED, }: raise ValueError("uncertain delivery has an invalid terminal outcome") + if self.terminal_verification is not None and ( + self.terminal_verification.payload.run_id != self.run_id + or self.terminal_verification.payload.run_report_sha256 + != self.report_sha256 + or self.terminal_verification.payload.run_report_object_sha256 + != self.report_sha256 + ): + raise ValueError("terminal verification names a different run report") return self @@ -411,13 +475,7 @@ def _subprocess_runner( ) report_path = run_dir / "report.json" report_bytes = report_path.read_bytes() if report_path.is_file() else None - proof_path = run_dir / "production-terminal-verification.json" - proof = None - if proof_path.is_file(): - proof = ProductionTerminalVerificationEnvelope.model_validate_json( - proof_path.read_bytes() - ) - return ManagedExecution(process.returncode, report_bytes, proof) + return ManagedExecution(process.returncode, report_bytes) class HostedRunnerAdapter: @@ -463,6 +521,13 @@ def _protected_runner_origin(config: RunnerConfig) -> str: raise ValueError("protected runner host is not one canonical HTTPS origin") return canonical + def protected_runner_origin(self, runner_config: Path) -> str: + """Return the origin from one protected, strictly parsed runner config.""" + + return self._protected_runner_origin( + load_runner_config(runner_config, protected=True) + ) + def registration_request( self, *, @@ -891,7 +956,7 @@ def _resolve_params( self, dispatch: HostedDispatch, config: RunnerConfig, - ) -> dict[str, str]: + ) -> dict[str, RuntimeParamScalar]: trusted = config.bundles.get(dispatch.payload.bundle.content_digest) if trusted is None: raise ValueError("hosted bundle is not locally trusted") @@ -955,16 +1020,17 @@ def _resolve_params( supplied = json.loads(raw.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise ValueError("parameter reference is not valid JSON") from exc - if not isinstance(supplied, dict) or any( - not isinstance(key, str) or not isinstance(value, str) - for key, value in supplied.items() - ): + if not isinstance(supplied, dict): raise ValueError("parameter reference has an invalid exact shape") inline = False + for name in supplied: + if not isinstance(name, str): + raise ValueError("runtime parameter name is invalid") + validate_runtime_param_name(name) return resolve_admitted_params(workflow, supplied, inline=inline) @staticmethod - def _write_params(path: Path, params: dict[str, str]) -> Path | None: + def _write_params(path: Path, params: dict[str, RuntimeParamScalar]) -> Path | None: if not params: return None flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL @@ -972,24 +1038,319 @@ def _write_params(path: Path, params: dict[str, str]) -> Path | None: flags |= os.O_NOFOLLOW descriptor = os.open(path, flags, 0o600) try: - raw = json.dumps(params, sort_keys=True, separators=(",", ":")).encode() + raw = json.dumps( + params, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode() os.write(descriptor, raw) os.fsync(descriptor) finally: os.close(descriptor) return path - @staticmethod - def _validate_terminal( + def _produce_terminal_verification( + self, + *, dispatch: HostedDispatch, - report_bytes: bytes, - proof: ProductionTerminalVerificationEnvelope, - ) -> None: - del dispatch, report_bytes, proof - raise ValueError( - "hosted production success requires a locally produced proof from " - "independent expected state and retained delivery receipts" + report: RunReport, + run_dir: Path, + qualification: ProductionQualificationAuthority, + private_key: Ed25519PrivateKey, + verified_params: dict[str, RuntimeParamScalar], + dispatch_binding_sha256: str, + ) -> tuple[ProductionTerminalVerificationEnvelope, str]: + """Build, retain, reread, and verify one exact terminal-v2 proof.""" + + store = CheckpointStore(run_dir) + manifest = store.read_manifest() + if ( + manifest is None + or manifest.delivery_authority_kind != "cloud_runner" + or manifest.remote_delivery_run_id != dispatch.run_id + or manifest.managed_dispatch_binding_sha256 != dispatch_binding_sha256 + or manifest.params != verified_params + ): + raise ValueError("retained managed run manifest differs from the dispatch") + authorization = manifest.governed_authorization + admission = qualification.qualification_admission + evidence_identity = admission.payload.evidence_identity + expected_production_binding = { + "production_qualification_admission_id": admission.payload.admission_id, + "production_qualification_admission_sha256": ( + qualification.qualification_admission_sha256 + ), + "production_qualification_evidence_identity_sha256": ( + evidence_identity.artifact_sha256() + ), + "production_qualification_runtime_validation_id": ( + qualification.expected.runtime_validation_id + ), + "production_qualification_signer_registry_sha256": ( + qualification.qualification_signer_registry_sha256 + ), + "production_qualification_signer_registry_revision": ( + qualification.qualification_signer_registry.revision + ), + "production_qualification_signer_registry_expires_at": ( + qualification.qualification_signer_registry.expires_at + ), + "production_qualification_authority_sha256": ( + qualification.immutable_binding_sha256() + ), + } + if authorization is None or any( + getattr(authorization, field) != value + for field, value in expected_production_binding.items() + ): + raise ValueError("retained production authority differs from admission") + if ( + dispatch_binding_sha256 != dispatch.payload.dispatch_binding_sha256 + or governed_dispatch_binding_sha256(dispatch.run_id, authorization) + != dispatch.payload.dispatch_binding_sha256 + ): + raise ValueError("retained governed authorization differs from dispatch") + qualification_case_id_sha256 = ( + None + if authorization.qualification_case_id is None + else hashlib.sha256( + authorization.qualification_case_id.encode("utf-8") + ).hexdigest() ) + authorized_effect_contracts = { + approval.step_id: list(approval.effect_contract_hashes) + for approval in authorization.unverified_write_approvals + } + if ( + report.params != verified_params + or report.governed_authorization_id != authorization.authorization_id + or report.governed_authorization_created_at != authorization.created_at + or report.governed_runtime_inputs_digest + != authorization.runtime_inputs_digest + or report.governed_policy_name != authorization.admitted_policy_name + or report.governed_policy_contract_sha256 + != authorization.admitted_policy_contract_sha256 + or report.governed_minimum_effect_tier != authorization.minimum_effect_tier + or report.governed_approval_source != authorization.approval_source + or report.execution_profile != authorization.execution_profile + or tuple(report.governed_qualified_effect_requirements) + != authorization.qualified_effect_requirements + or tuple(report.required_identity_step_ids) + != authorization.required_identity_step_ids + or report.approved_unverified_effect_step_ids + != [item.step_id for item in authorization.unverified_write_approvals] + or report.governed_authorized_effect_contracts + != authorized_effect_contracts + or report.governed_qualification_project_id + != authorization.qualification_project_id + or report.governed_qualification_project_revision + != authorization.qualification_project_revision + or report.governed_qualification_project_contract_sha256 + != authorization.qualification_project_contract_sha256 + or report.governed_qualification_campaign_id_sha256 + != authorization.qualification_campaign_id_sha256 + or report.governed_qualification_case_id_sha256 + != qualification_case_id_sha256 + or report.governed_qualification_case_input_sha256 + != authorization.qualification_case_input_sha256 + or report.governed_qualification_run_id_sha256 + != authorization.qualification_run_id_sha256 + or report.governed_qualification_case_kind + != authorization.qualification_case_kind + or report.governed_qualification_case_action_paths + != authorization.qualification_case_action_paths + or report.governed_qualification_fault_driver_id + != authorization.qualification_fault_driver_id + or report.governed_qualification_fault_driver_contract_sha256 + != authorization.qualification_fault_driver_contract_sha256 + or report.governed_qualification_fault_driver_key_id + != authorization.qualification_fault_driver_key_id + or report.governed_qualification_fault_step_id_sha256 + != authorization.qualification_fault_step_id_sha256 + ): + raise ValueError("run report differs from retained governed inputs") + + chain = DurableAuthority(run_dir, store).production_delivery_permit_chain() + first = chain.entries[0] + expected = qualification.expected + runtime = expected.runtime_build_identity + flow_run_id_sha256 = hashlib.sha256(dispatch.run_id.encode("utf-8")).hexdigest() + runner_id_sha256 = hashlib.sha256( + dispatch.runner_id.encode("utf-8") + ).hexdigest() + runner_session_id_sha256 = hashlib.sha256( + dispatch.runner_session_id.encode("utf-8") + ).hexdigest() + if ( + first.run_id != dispatch.run_id + or first.flow_run_id_sha256 != flow_run_id_sha256 + or first.admission_artifact_sha256 + != qualification.qualification_admission_sha256 + or first.evidence_identity_sha256 + != admission.payload.evidence_identity.artifact_sha256() + or first.environment_digest != expected.environment_digest + or first.qualification_signer_registry_sha256 + != qualification.qualification_signer_registry_sha256 + or first.qualification_signer_registry_revision + != qualification.qualification_signer_registry.revision + or first.authenticated_runner_id_sha256 != runner_id_sha256 + or first.authenticated_session_id_sha256 != runner_session_id_sha256 + ): + raise ValueError("retained delivery chain differs from admitted live state") + + prepared = prepare_production_terminal_evidence(report) + now = datetime.now(timezone.utc).replace(microsecond=0) + now_text = now.isoformat().replace("+00:00", "Z") + context = ProductionTerminalVerificationContext( + run_id=dispatch.run_id, + tenant_id=dispatch.tenant_id, + workflow_id=dispatch.workflow_id, + workflow_version_id=dispatch.workflow_version_id, + bundle_version_id=dispatch.workflow_version_id, + bundle_artifact_sha256=expected.bundle_artifact_sha256, + environment_digest=expected.environment_digest, + environment_contract_sha256=expected.environment_contract_sha256, + runtime_environment_sha256=expected.runtime_environment_sha256, + identity_contract_sha256=expected.identity_contract_sha256, + effect_contract_sha256=expected.effect_contract_sha256, + runtime_validation_id=expected.runtime_validation_id, + runtime_substrate=runtime.substrate, + admission_id=admission.payload.admission_id, + admission_artifact_sha256=(qualification.qualification_admission_sha256), + admission_policy_sha256=evidence_identity.admission_policy_sha256, + evidence_identity_sha256=evidence_identity.artifact_sha256(), + admitted_runtime_build_sha256=runtime.artifact_sha256(), + evidence_runner_signer_sha256=expected.evidence_runner_signer_sha256, + qualification_signer_registry_sha256=( + qualification.qualification_signer_registry_sha256 + ), + qualification_signer_registry_revision=( + qualification.qualification_signer_registry.revision + ), + execution_authority_id=first.execution_authority_id, + execution_authority_sha256=first.execution_authority_sha256, + execution_authority_signer_sha256=first.authority_signer_sha256, + permit_chain=chain, + run_report_object_version="sha256:" + prepared.report_sha256, + verified_at=now_text, + issued_at=now_text, + ) + built = build_production_terminal_verification( + report, + context=context, + private_key=private_key, + ) + if ( + built.report_bytes != prepared.report_bytes + or built.report_sha256 != prepared.report_sha256 + ): + raise ValueError("terminal report changed during proof production") + envelope_bytes = canonical_json(built.envelope) + payload = built.envelope.payload + final = chain.entries[-1] + live_expected = ProductionTerminalVerificationExpected( + run_id=dispatch.run_id, + flow_run_id_sha256=flow_run_id_sha256, + tenant_id=dispatch.tenant_id, + workflow_id=dispatch.workflow_id, + workflow_version_id=dispatch.workflow_version_id, + bundle_version_id=dispatch.workflow_version_id, + bundle_artifact_sha256=expected.bundle_artifact_sha256, + bundle_content_digest=expected.bundle_content_digest, + environment_digest=expected.environment_digest, + environment_contract_sha256=expected.environment_contract_sha256, + runtime_environment_sha256=expected.runtime_environment_sha256, + identity_contract_sha256=expected.identity_contract_sha256, + effect_contract_sha256=expected.effect_contract_sha256, + runtime_validation_id=expected.runtime_validation_id, + runtime_substrate=runtime.substrate, + admission_id=admission.payload.admission_id, + admission_artifact_sha256=(qualification.qualification_admission_sha256), + admission_policy_sha256=evidence_identity.admission_policy_sha256, + evidence_identity_sha256=evidence_identity.artifact_sha256(), + admitted_runtime_build_sha256=runtime.artifact_sha256(), + evidence_runner_signer_sha256=expected.evidence_runner_signer_sha256, + qualification_signer_registry_sha256=( + qualification.qualification_signer_registry_sha256 + ), + qualification_signer_registry_revision=( + qualification.qualification_signer_registry.revision + ), + execution_authority_id=first.execution_authority_id, + execution_authority_sha256=first.execution_authority_sha256, + execution_authority_signer_sha256=first.authority_signer_sha256, + permit_chain_sha256=chain.permit_chain_sha256, + permit_count=len(chain.entries), + final_authority_sequence=final.authority_sequence, + final_runtime_delivery_sequence=final.runtime_delivery_sequence, + authenticated_runner_id_sha256=runner_id_sha256, + authenticated_session_id_sha256=runner_session_id_sha256, + acknowledged_one_use_claim_ids=tuple( + item.one_use_claim_id for item in chain.entries + ), + workflow_contract_sha256=payload.workflow_contract_sha256, + execution_outcome_sha256=payload.execution_outcome_sha256, + run_receipt_sha256=payload.run_receipt_sha256, + run_report_sha256=built.report_sha256, + run_report_object_version=context.run_report_object_version, + run_report_object_sha256=built.report_sha256, + evidence_manifests=payload.evidence_manifests, + ) + artifact_sha256 = verify_production_terminal_verification_from_report( + built.envelope, + report_bytes=built.report_bytes, + expected=live_expected, + now=now, + ) + if artifact_sha256 != hashlib.sha256(envelope_bytes).hexdigest(): + raise ValueError("terminal verification artifact digest changed") + + # Final-named evidence exists only after the complete in-memory proof + # passes. If storage or the required reread fails, remove only files + # created by this call so no failed terminalization leaves success + # artifacts behind. + report_path = run_dir / "production-terminal-report.json" + envelope_path = run_dir / "production-terminal-verification.json" + written: list[Path] = [] + try: + self._write_private_bytes(report_path, built.report_bytes) + written.append(report_path) + self._write_private_bytes(envelope_path, envelope_bytes) + written.append(envelope_path) + stored_report = self._read_private_bytes( + report_path, + maximum_bytes=_MAX_ARTIFACT_BYTES, + label="production terminal report", + ) + stored_envelope = self._read_private_bytes( + envelope_path, + maximum_bytes=_MAX_ARTIFACT_BYTES, + label="production terminal verification", + ) + if stored_report != built.report_bytes or stored_envelope != envelope_bytes: + raise ValueError("stored terminal evidence changed after write") + reread = ProductionTerminalVerificationEnvelope.model_validate_json( + stored_envelope + ) + if canonical_json(reread) != stored_envelope: + raise ValueError("stored terminal verification is not canonical") + reread_sha256 = verify_production_terminal_verification_from_report( + reread, + report_bytes=stored_report, + expected=live_expected, + now=now, + ) + if reread_sha256 != artifact_sha256: + raise ValueError("stored terminal verification digest changed") + except Exception: + for path in reversed(written): + try: + path.unlink() + except OSError: + pass + raise + return reread, built.report_sha256 @staticmethod def _refusal( @@ -1256,23 +1617,47 @@ def execute( report_sha256="0" * 64, ) report: RunReport | None = None + report_digest = hashlib.sha256(execution.report_bytes).hexdigest() try: report = RunReport.model_validate_json(execution.report_bytes) outcome = classify_transaction_outcome(report) - proof = execution.terminal_verification + proof: ProductionTerminalVerificationEnvelope | None = None + if execution.terminal_verification is not None: + raise ValueError("managed child supplied an untrusted terminal proof") if outcome is TransactionOutcome.VERIFIED: - if proof is None: - outcome = TransactionOutcome.RECONCILIATION_REQUIRED - else: - self._validate_terminal(parsed, execution.report_bytes, proof) - elif proof is not None: - raise ValueError("non-VERIFIED execution supplied a success proof") - except ValueError: + if execution.returncode != 0: + raise ValueError("managed child exited unsuccessfully") + terminal_config = load_runner_config(runner_config, protected=True) + if self._protected_runner_origin(terminal_config) != configured_origin: + raise ValueError("protected runner origin changed during execution") + self._verify_product_release(parsed, terminal_config) + terminal_key = self._load_evidence_private_key(terminal_config) + terminal_qualification, terminal_deployment = ( + self._verify_workflow_admission( + parsed, + terminal_config, + evidence_private_key=terminal_key, + ) + ) + if ( + terminal_qualification != qualification + or terminal_deployment != deployment_bytes + ): + raise ValueError("production admission changed during execution") + proof, report_digest = self._produce_terminal_verification( + dispatch=parsed, + report=report, + run_dir=run_dir, + qualification=qualification, + private_key=terminal_key, + verified_params=params, + dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, + ) + except Exception: # noqa: BLE001 - post-delivery terminalization fails closed outcome = TransactionOutcome.RECONCILIATION_REQUIRED proof = None if outcome is not TransactionOutcome.VERIFIED: proof = None - report_digest = hashlib.sha256(execution.report_bytes).hexdigest() self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) if report is None: events = tuple( @@ -1313,24 +1698,33 @@ def execute( def callback_request( self, - dispatch: HostedDispatch, + dispatch: HostedDispatch | HostedRecoveryBinding | Mapping[str, object], result: HostedRunResult | HostedDispatchRefusal, ) -> CallbackRequest: - if ( - result.dispatch_id != dispatch.dispatch_id - or result.run_id != dispatch.run_id - ): + if isinstance(dispatch, HostedDispatch): + binding: HostedDispatch | HostedRecoveryBinding = dispatch + elif isinstance(dispatch, HostedRecoveryBinding): + binding = dispatch + else: + schema = dispatch.get("schema_version") + if schema == "openadapt.hosted-runner-recovery/v1": + binding = HostedRecoveryBinding.model_validate(dispatch) + else: + binding = HostedDispatch.model_validate(dispatch) + if result.dispatch_id != binding.dispatch_id or result.run_id != binding.run_id: raise ValueError("hosted result does not bind the callback lease") events = list(result.evidence_batch) - proof_bytes = None + proof_base64 = None proof_digest = None if isinstance(result, HostedRunResult): if result.terminal_verification is not None: - raise ValueError( - "the full local v2 proof cannot cross the hosted callback boundary" - ) + proof_bytes = canonical_json(result.terminal_verification) + proof_digest = result.terminal_verification.artifact_sha256() + if hashlib.sha256(proof_bytes).hexdigest() != proof_digest: + raise ValueError("terminal proof digest differs from exact bytes") + proof_base64 = b64encode(proof_bytes).decode("ascii") terminal = HostedTerminalEvent( - run_id=dispatch.run_id, + run_id=binding.run_id, outcome=( result.outcome.value if isinstance(result.outcome, TransactionOutcome) @@ -1339,18 +1733,24 @@ def callback_request( report_sha256=result.report_sha256, started=result.started, uncertain_delivery=result.uncertain_delivery, - terminal_verification_artifact_bytes_base64=proof_bytes, + terminal_verification_artifact_bytes_base64=proof_base64, terminal_verification_artifact_sha256=proof_digest, ) events.append(terminal.model_dump(mode="json")) return CallbackRequest( - dispatch_id=dispatch.dispatch_id, - runner_session_id=dispatch.runner_session_id, - idempotency_key=dispatch.idempotency_key, - lease_token=dispatch.lease_token, + dispatch_id=binding.dispatch_id, + runner_session_id=binding.runner_session_id, + idempotency_key=binding.idempotency_key, + lease_token=binding.lease_token, product_release_admission_sha256=( - dispatch.product_release_admission.artifact_sha256 + binding.product_release_admission.artifact_sha256 + if isinstance(binding, HostedDispatch) + else binding.product_release_admission_sha256 + ), + workflow_admission_sha256=( + binding.workflow_admission.artifact_sha256 + if isinstance(binding, HostedDispatch) + else binding.workflow_admission_sha256 ), - workflow_admission_sha256=dispatch.workflow_admission.artifact_sha256, events=tuple(events), ) diff --git a/openadapt_flow/runner/inputs.py b/openadapt_flow/runner/inputs.py index 977fdab8..48d1ebf1 100644 --- a/openadapt_flow/runner/inputs.py +++ b/openadapt_flow/runner/inputs.py @@ -6,20 +6,28 @@ from datetime import date from openadapt_flow.ir import ParamKind, ParamSpec, Workflow -from openadapt_flow.runtime.authorization import effective_runtime_params +from openadapt_flow.runtime.authorization import ( + RuntimeParamScalar, + effective_runtime_params, + is_runtime_param_scalar, +) class AdmittedInputError(ValueError): """Hosted inputs do not fit the exact schema sealed into the workflow.""" -def _validate_value(spec: ParamSpec, value: str) -> None: +def _validate_value(spec: ParamSpec, value: RuntimeParamScalar) -> None: if spec.type is ParamKind.ENUM: - if not spec.choices or value not in spec.choices: + if not isinstance(value, str) or not spec.choices or value not in spec.choices: raise AdmittedInputError( f"parameter {spec.name!r} is outside its admitted enum" ) elif spec.type is ParamKind.DATE: + if not isinstance(value, str): + raise AdmittedInputError( + f"parameter {spec.name!r} is not an ISO date string" + ) try: parsed = date.fromisoformat(value) except ValueError as exc: @@ -31,22 +39,24 @@ def _validate_value(spec: ParamSpec, value: str) -> None: f"parameter {spec.name!r} is not a canonical ISO date" ) elif spec.type is ParamKind.NUMBER: - try: - number = float(value) - except ValueError as exc: - raise AdmittedInputError( - f"parameter {spec.name!r} is not a number" - ) from exc + if type(value) not in {int, float}: + raise AdmittedInputError(f"parameter {spec.name!r} is not a number") + number = float(value) if not math.isfinite(number): raise AdmittedInputError(f"parameter {spec.name!r} must be a finite number") + elif spec.type is ParamKind.BOOLEAN: + if type(value) is not bool: + raise AdmittedInputError(f"parameter {spec.name!r} is not a Boolean") + elif not isinstance(value, str): + raise AdmittedInputError(f"parameter {spec.name!r} is not a string") def resolve_admitted_params( workflow: Workflow, - supplied: dict[str, str], + supplied: dict[str, RuntimeParamScalar], *, inline: bool, -) -> dict[str, str]: +) -> dict[str, RuntimeParamScalar]: """Resolve exact hosted params without inventing or widening the schema. Hosted execution requires the sealed typed schema. Inline input can never @@ -89,7 +99,9 @@ def resolve_admitted_params( if spec.required: raise AdmittedInputError(f"required parameter {name!r} is missing") continue - if not isinstance(value, str): - raise AdmittedInputError(f"parameter {name!r} is not a string value") + if not is_runtime_param_scalar(value): + raise AdmittedInputError( + f"parameter {name!r} is not a finite JSON scalar value" + ) _validate_value(spec, value) return resolved diff --git a/openadapt_flow/runner/protocol.py b/openadapt_flow/runner/protocol.py index be85f9f5..018c223d 100644 --- a/openadapt_flow/runner/protocol.py +++ b/openadapt_flow/runner/protocol.py @@ -17,11 +17,17 @@ import hashlib import json +import re from typing import Union from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator -from openadapt_flow.runtime.authorization import GovernedRunAuthorization +from openadapt_flow.runtime.authorization import ( + GovernedRunAuthorization, + RuntimeParamScalar, + is_runtime_param_scalar, + normalize_runtime_param_scalar, +) #: The only job kind the v1 client executes. JOB_KIND_GOVERNED_RUN = "governed_run" @@ -34,6 +40,21 @@ #: Cloud long-poll ceiling (runners.ts POLL_MAX_WAIT_S). POLL_MAX_WAIT_S = 25 +# This is the exact closed grammar used by Cloud's production runtime schema. +# Keeping hosted parameter keys in ASCII also makes Python and JavaScript key +# ordering byte-identical for the authorization digest. +RUNTIME_PARAM_NAME_PATTERN = r"^[A-Za-z_][A-Za-z0-9_]{0,127}$" +_RUNTIME_PARAM_NAME = re.compile(RUNTIME_PARAM_NAME_PATTERN) + + +def validate_runtime_param_name(name: str) -> str: + """Refuse a hosted parameter name outside the shared Cloud grammar.""" + + if _RUNTIME_PARAM_NAME.fullmatch(name) is None: + raise ValueError("runtime parameter name is invalid") + return name + + #: Runtime-local v2 authority bindings are deliberately OUTSIDE the dispatch #: binding digest. The digest grammar is a cross-repo contract: Cloud and Flow #: must hash byte-identical payloads, so fields Cloud does not emit may not @@ -110,7 +131,20 @@ class DispatchParamsValues(BaseModel): model_config = ConfigDict(frozen=True, extra="forbid") - values: dict[str, str] + values: dict[str, RuntimeParamScalar] = Field(max_length=100) + + @model_validator(mode="after") + def _finite_json_scalars(self) -> "DispatchParamsValues": + normalized: dict[str, RuntimeParamScalar] = {} + for key, value in self.values.items(): + validate_runtime_param_name(key) + if not is_runtime_param_scalar(value): + raise ValueError( + "runtime parameters must be finite JSON-safe scalar values" + ) + normalized[key] = normalize_runtime_param_scalar(value) + object.__setattr__(self, "values", normalized) + return self class DispatchParamsRef(BaseModel): diff --git a/openadapt_flow/runner/verify.py b/openadapt_flow/runner/verify.py index 8819b0b1..996b2d96 100644 --- a/openadapt_flow/runner/verify.py +++ b/openadapt_flow/runner/verify.py @@ -49,6 +49,10 @@ DispatchParamsValues, RunnerDispatchPayload, ) +from openadapt_flow.runtime.authorization import ( + RuntimeParamScalar, + runtime_param_text, +) if TYPE_CHECKING: # pragma: no cover - typing only from pathlib import Path @@ -119,7 +123,7 @@ class VerifiedDispatch: payload: RunnerDispatchPayload bundle: TrustedBundle profile_path: "Path" - params: dict[str, str] + params: dict[str, RuntimeParamScalar] workflow: "Workflow" #: Whole-workflow coverage counts for the terminal run_summary (computed #: from the sealed bundle, not from the cloud's claims). @@ -143,7 +147,7 @@ def verify_dispatch( *, now: Optional[datetime] = None, active_workflow_ids: Optional[set[str]] = None, - resolved_params: Optional[dict[str, str]] = None, + resolved_params: Optional[dict[str, RuntimeParamScalar]] = None, ) -> VerifiedDispatch | Refusal: """Independently verify ``payload`` against local trust. Never executes. @@ -245,7 +249,7 @@ def verify_dispatch( RefusalCode.PARAM_DOMAIN_REFUSED, f"param {key!r} has no operator-pinned domain pattern", ) - if re.fullmatch(pattern, params[key]) is None: + if re.fullmatch(pattern, runtime_param_text(params[key])) is None: return Refusal( RefusalCode.PARAM_DOMAIN_REFUSED, f"param {key!r} does not match its pinned domain pattern", diff --git a/openadapt_flow/runtime/authorization.py b/openadapt_flow/runtime/authorization.py index 5e814c85..a59e8e82 100644 --- a/openadapt_flow/runtime/authorization.py +++ b/openadapt_flow/runtime/authorization.py @@ -10,11 +10,12 @@ import hashlib import json +import math import re import threading from datetime import datetime, timezone from pathlib import Path -from typing import Literal +from typing import Literal, Mapping, TypeAlias, Union, cast from uuid import uuid4 from pydantic import BaseModel, ConfigDict, Field, model_validator @@ -30,22 +31,167 @@ _CONSUMED_LOCK = threading.Lock() _QUALIFICATION_ID_RE = r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" +RuntimeParamScalar: TypeAlias = Union[str, bool, int, float] +_JS_SAFE_INTEGER = 9_007_199_254_740_991 -def effective_runtime_params( - workflow: Workflow, supplied: dict[str, str] | None + +def is_runtime_param_scalar(value: object) -> bool: + """Return whether ``value`` is one exact supported finite JSON scalar.""" + + if type(value) in {str, bool}: + return True + if type(value) is int: + if -_JS_SAFE_INTEGER <= value <= _JS_SAFE_INTEGER: + return True + # JSON has one Number type. Admit a large integer spelling only when + # the JavaScript renderer of its finite IEEE-754 value returns that + # same spelling. The normalizer converts it to that Number before any + # retained artifact. + try: + number = float(value) + except OverflowError: + return False + return math.isfinite(number) and _javascript_number_text(number) == str(value) + return type(value) is float and math.isfinite(value) + + +def normalize_runtime_param_scalar(value: object) -> RuntimeParamScalar: + """Return one exact finite JSON scalar in its cross-language representation.""" + + if not is_runtime_param_scalar(value): + raise ValueError("runtime parameter is not a finite interoperable JSON scalar") + if type(value) is int and not -_JS_SAFE_INTEGER <= value <= _JS_SAFE_INTEGER: + return float(value) + return cast(RuntimeParamScalar, value) + + +def _javascript_number_text(value: int | float) -> str: + """Return the finite number text used by JavaScript ``JSON.stringify``. + + The hosted service uses ``JSON.stringify`` for primitive values in its + sorted-key canonical JSON. Python and JavaScript choose different exponent + formatting thresholds, so ordinary ``json.dumps`` is not interoperable. + An out-of-safe-range Python integer reaches this function only when its + spelling is the canonical JavaScript rendering of the normalized Number. + """ + + if type(value) is int: + if not is_runtime_param_scalar(value): + raise ValueError("integer parameter exceeds the JSON safe-integer range") + if not -_JS_SAFE_INTEGER <= value <= _JS_SAFE_INTEGER: + return _javascript_number_text(float(value)) + return str(value) + if type(value) is not float or not math.isfinite(value): + raise ValueError("number parameter must be finite") + if value == 0: + return "0" + + negative = value < 0 + source = repr(abs(value)).lower() + mantissa, separator, exponent_text = source.partition("e") + exponent = int(exponent_text) if separator else 0 + integer, dot, fraction = mantissa.partition(".") + digits = integer + (fraction if dot else "") + decimal_exponent = exponent - len(fraction) + while len(digits) > 1 and digits.endswith("0"): + digits = digits[:-1] + decimal_exponent += 1 + decimal_point = len(digits) + decimal_exponent + + absolute = abs(value) + if 1e-6 <= absolute < 1e21: + if decimal_point <= 0: + rendered = "0." + ("0" * -decimal_point) + digits + elif decimal_point >= len(digits): + rendered = digits + ("0" * (decimal_point - len(digits))) + else: + rendered = digits[:decimal_point] + "." + digits[decimal_point:] + else: + scientific_exponent = decimal_point - 1 + rendered = digits[0] + if len(digits) > 1: + rendered += "." + digits[1:] + rendered += "e" + ("+" if scientific_exponent >= 0 else "") + rendered += str(scientific_exponent) + return ("-" if negative else "") + rendered + + +def _hosted_canonical_json(value: object) -> str: + """Match the hosted sorted-key ``canonicalJson`` implementation exactly.""" + + if value is None: + return "null" + if type(value) is bool: + return "true" if value else "false" + if type(value) in {int, float}: + return _javascript_number_text(cast(Union[int, float], value)) + if isinstance(value, str): + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + if isinstance(value, list): + return "[" + ",".join(_hosted_canonical_json(item) for item in value) + "]" + if isinstance(value, dict): + if any(not isinstance(key, str) for key in value): + raise ValueError("canonical JSON object keys must be strings") + # JavaScript Array.sort compares UTF-16 code units. Python's default + # string order compares Unicode code points, which differs for astral + # characters versus BMP characters at U+D800 and above. + ordered_keys = sorted(value, key=lambda key: key.encode("utf-16-be")) + members = ( + json.dumps(key, ensure_ascii=False) + + ":" + + _hosted_canonical_json(value[key]) + for key in ordered_keys + ) + return "{" + ",".join(members) + "}" + raise ValueError(f"unsupported canonical JSON value: {type(value).__name__}") + + +def runtime_param_text(value: RuntimeParamScalar) -> str: + """Render one admitted scalar only at the final GUI text boundary.""" + + value = normalize_runtime_param_scalar(value) + if isinstance(value, str): + return value + if type(value) is bool: + return "true" if value else "false" + return _javascript_number_text(value) + + +def runtime_params_for_gui( + params: Mapping[str, RuntimeParamScalar], ) -> dict[str, str]: + """Convert the already-authorized typed parameter set to GUI text.""" + + return {name: runtime_param_text(value) for name, value in params.items()} + + +def effective_runtime_params( + workflow: Workflow, supplied: Mapping[str, RuntimeParamScalar] | None +) -> dict[str, RuntimeParamScalar]: """Resolve defaults exactly as :meth:`Replayer.run` does.""" - merged = dict(workflow.params) + merged: dict[str, RuntimeParamScalar] = dict(workflow.params) for name, spec in workflow.param_specs.items(): if spec.example is not None: merged.setdefault(name, spec.example) merged.update(supplied or {}) - return merged + try: + return { + name: normalize_runtime_param_scalar(value) + for name, value in merged.items() + } + except ValueError as exc: + invalid = [ + name for name, value in merged.items() if not is_runtime_param_scalar(value) + ] + raise ValueError( + "runtime parameters must be strings, Booleans, or finite JSON-safe " + "numbers: " + ", ".join(sorted(invalid)) + ) from exc def runtime_inputs_bytes( workflow: Workflow, - params: dict[str, str] | None, + params: Mapping[str, RuntimeParamScalar] | None, worklists: dict[str, list[dict[str, str]]] | None, *, interstitials: list[Interstitial] | None = None, @@ -64,9 +210,7 @@ def runtime_inputs_bytes( payload["interstitials"] = [ interstitial.model_dump(mode="json") for interstitial in interstitials ] - canonical = json.dumps( - payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False - ) + canonical = _hosted_canonical_json(payload) return canonical.encode("utf-8") @@ -74,7 +218,7 @@ def parse_runtime_inputs_bytes( value: bytes, *, workflow: Workflow, -) -> tuple[dict[str, str], dict[str, list[dict[str, str]]]]: +) -> tuple[dict[str, RuntimeParamScalar], dict[str, list[dict[str, str]]]]: """Parse bytes that this workflow's runtime serializer can emit exactly.""" try: @@ -88,7 +232,7 @@ def parse_runtime_inputs_bytes( params = payload.get("params") worklists = payload.get("worklists") if not isinstance(params, dict) or any( - not isinstance(key, str) or not isinstance(item, str) + not isinstance(key, str) or not is_runtime_param_scalar(item) for key, item in params.items() ): raise ValueError("runtime-input artifact has invalid parameters") @@ -135,12 +279,12 @@ def parse_runtime_inputs_bytes( ) if canonical != value: raise ValueError("runtime-input artifact is not in canonical form") - return dict(params), canonical_worklists + return effective_runtime_params(workflow, dict(params)), canonical_worklists def runtime_inputs_digest( workflow: Workflow, - params: dict[str, str] | None, + params: Mapping[str, RuntimeParamScalar] | None, worklists: dict[str, list[dict[str, str]]] | None, *, interstitials: list[Interstitial] | None = None, @@ -692,7 +836,7 @@ def validate_execution( workflow: Workflow, *, bundle_dir: Path | str, - params: dict[str, str] | None, + params: Mapping[str, RuntimeParamScalar] | None, worklists: dict[str, list[dict[str, str]]] | None, interstitials: list[Interstitial] | None = None, continuation: bool = False, @@ -713,7 +857,7 @@ def validate_execution_snapshot( workflow: Workflow, *, bundle_dir: Path | str, - params: dict[str, str] | None, + params: Mapping[str, RuntimeParamScalar] | None, worklists: dict[str, list[dict[str, str]]] | None, interstitials: list[Interstitial] | None = None, continuation: bool = False, diff --git a/openadapt_flow/runtime/durable/attended.py b/openadapt_flow/runtime/durable/attended.py index 75fb208b..0afde4dc 100644 --- a/openadapt_flow/runtime/durable/attended.py +++ b/openadapt_flow/runtime/durable/attended.py @@ -49,6 +49,7 @@ effects_for_actuation, project_step_safety, ) +from openadapt_flow.runtime.authorization import RuntimeParamScalar from openadapt_flow.runtime.durable.approval import ( ApprovalRecord, ApprovalRequired, @@ -2749,7 +2750,7 @@ def _validated_attended_result( *, identity: Optional[IdentityCheck], skipped: bool, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], manifest: Any, ) -> StepResult: """Return the exact evidence shape emitted by an attended completion.""" @@ -2813,7 +2814,7 @@ def checkpoint_human_completed_step( capability: AttendedPauseCapability, approval: ApprovalRecord, result: StepResult, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], key: Optional[str] = None, ) -> RunCheckpoint: """Advance a linear resume point after outcome verification, without acting.""" @@ -3344,7 +3345,7 @@ def _program_context( store: CheckpointStore, workflow: Workflow, capability: AttendedPauseCapability, - ) -> tuple[PendingEscalation, State, dict[str, str]]: + ) -> tuple[PendingEscalation, State, dict[str, RuntimeParamScalar]]: pending = store.read_pending() state = _program_pause_state(workflow, pending) if pending is not None else None if ( @@ -3370,7 +3371,7 @@ def _resume_program( approval: ApprovalRecord, pending: PendingEscalation, state: State, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], result: StepResult, skipped: bool, target_state_id: Optional[str], @@ -3753,7 +3754,12 @@ def _continue_run_locked( approval: ApprovalRecord, ) -> AttendedExecutionResult: program_context: Optional[ - tuple[PendingEscalation, State, dict[str, str], Optional[str]] + tuple[ + PendingEscalation, + State, + dict[str, RuntimeParamScalar], + Optional[str], + ] ] = None try: store, manifest, workflow = self._load(run_dir, capability) @@ -3907,7 +3913,12 @@ def _reconcile_run_locked( ) reconciliation_delivery_state = capability.delivery_state program_context: Optional[ - tuple[PendingEscalation, State, dict[str, str], Optional[str]] + tuple[ + PendingEscalation, + State, + dict[str, RuntimeParamScalar], + Optional[str], + ] ] = None try: store, manifest, workflow = self._load(run_dir, capability) @@ -4087,7 +4098,12 @@ def _skip_run_locked( approval: ApprovalRecord, ) -> AttendedExecutionResult: program_context: Optional[ - tuple[PendingEscalation, State, dict[str, str], Optional[str]] + tuple[ + PendingEscalation, + State, + dict[str, RuntimeParamScalar], + Optional[str], + ] ] = None try: store, manifest, workflow = self._load(run_dir, capability) diff --git a/openadapt_flow/runtime/durable/business_decision.py b/openadapt_flow/runtime/durable/business_decision.py index 3d52b81c..858264fe 100644 --- a/openadapt_flow/runtime/durable/business_decision.py +++ b/openadapt_flow/runtime/durable/business_decision.py @@ -27,6 +27,7 @@ ProgramExecutionScopeFrame, Workflow, ) +from openadapt_flow.runtime.authorization import RuntimeParamScalar from openadapt_flow.runtime.durable.approval import ( ApprovalRecord, ApprovalRequired, @@ -806,7 +807,7 @@ def issue( graph_id: str, state_id: str, frames: list[GraphFrame], - params: dict[str, str], + params: dict[str, RuntimeParamScalar], spec: BusinessDecisionSpec, governed_runtime_inputs_digest: str | None, now: datetime | None = None, @@ -933,7 +934,7 @@ def _validate_live_binding( graph_id: str, state_id: str, frames: list[GraphFrame], - params: dict[str, str], + params: dict[str, RuntimeParamScalar], spec: BusinessDecisionSpec, governed_runtime_inputs_digest: str | None, ) -> None: @@ -1183,7 +1184,7 @@ def consume( graph_id: str, state_id: str, frames: list[GraphFrame], - params: dict[str, str], + params: dict[str, RuntimeParamScalar], spec: BusinessDecisionSpec, governed_runtime_inputs_digest: str | None, now: datetime | None = None, diff --git a/openadapt_flow/runtime/durable/checkpoint.py b/openadapt_flow/runtime/durable/checkpoint.py index 6072cf9c..aa69f853 100644 --- a/openadapt_flow/runtime/durable/checkpoint.py +++ b/openadapt_flow/runtime/durable/checkpoint.py @@ -62,7 +62,10 @@ ProgramTransitionEvidence, Resolution, ) -from openadapt_flow.runtime.authorization import GovernedRunAuthorization +from openadapt_flow.runtime.authorization import ( + GovernedRunAuthorization, + RuntimeParamScalar, +) from openadapt_flow.runtime.durable.approval import ApprovalRecord from openadapt_flow.runtime.durable.program_checkpoint import ( GraphFrame, @@ -115,7 +118,7 @@ class RunManifest(BaseModel): bundle_dir: str #: The run's fully-resolved parameter bindings (defaults + caller #: overrides), so a resume re-binds identically. - params: dict[str, str] = Field(default_factory=dict) + params: dict[str, RuntimeParamScalar] = Field(default_factory=dict) #: Stable at-most-once reservation for the complete logical durable run. #: A resumed leg reuses it only after it proves ownership in the ledger. idempotency_key: Optional[str] = None @@ -206,7 +209,7 @@ class RunCheckpoint(BaseModel): #: point at an arbitrary successor state. next_step_index: int #: The run's parameter bindings at checkpoint time (resume re-binds these). - params: dict[str, str] = Field(default_factory=dict) + params: dict[str, RuntimeParamScalar] = Field(default_factory=dict) #: Verification evidence carried for the audit trail / operator. effect_verified: Optional[bool] = None effect_approved_unverified: bool = False @@ -295,7 +298,7 @@ class PendingEscalation(BaseModel): resume_from_index: int = 0 resume_from_step_id: Optional[str] = None #: The run's parameter bindings, so an approved resume re-binds identically. - params: dict[str, str] = Field(default_factory=dict) + params: dict[str, RuntimeParamScalar] = Field(default_factory=dict) #: ``rejected`` is TERMINAL: an operator answered the attended pause with #: ``reject``, asserting this run must not proceed. The file is retained #: rather than cleared so the audit trail keeps WHY the run stopped and diff --git a/openadapt_flow/runtime/durable/controller.py b/openadapt_flow/runtime/durable/controller.py index e1994a84..1b5c8ccd 100644 --- a/openadapt_flow/runtime/durable/controller.py +++ b/openadapt_flow/runtime/durable/controller.py @@ -43,7 +43,10 @@ StepResult, Workflow, ) -from openadapt_flow.runtime.authorization import GovernedRunAuthorization +from openadapt_flow.runtime.authorization import ( + GovernedRunAuthorization, + RuntimeParamScalar, +) from openadapt_flow.runtime.durable.approval import StateDiverged from openadapt_flow.runtime.durable.checkpoint import ( CheckpointStore, @@ -262,7 +265,7 @@ def __init__( run_id: str, workflow_name: str, bundle_dir: Path | str, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], worklists: dict[str, list[dict[str, str]]], idempotency_key: Optional[str] = None, save_healed_to: Optional[Path | str] = None, @@ -452,7 +455,7 @@ def record( step_index: int, step: Step, result: StepResult, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], *, workflow: Optional[Workflow] = None, transition_observation: Optional["TransitionObservation"] = None, @@ -617,7 +620,7 @@ def record_program_halt( state_id: str, intent: str, result: StepResult, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], workflow: Optional[Workflow] = None, transition_observation: Optional["TransitionObservation"] = None, program_frames: Optional[list[GraphFrame]] = None, diff --git a/openadapt_flow/runtime/durable/program_checkpoint.py b/openadapt_flow/runtime/durable/program_checkpoint.py index 40b91bb3..f9bd5111 100644 --- a/openadapt_flow/runtime/durable/program_checkpoint.py +++ b/openadapt_flow/runtime/durable/program_checkpoint.py @@ -20,6 +20,7 @@ import hashlib import json +from collections.abc import Mapping from datetime import datetime, timezone from pathlib import Path from typing import Any, Literal, Optional @@ -40,6 +41,7 @@ ProgramTransitionEvidence, Resolution, ) +from openadapt_flow.runtime.authorization import RuntimeParamScalar #: The synthetic ``graph_id`` of the top-level ``Workflow.program`` graph (every #: OTHER graph is a named entry in ``Workflow.subflows`` -- including a loop @@ -142,7 +144,7 @@ class GraphFrame(BaseModel): state_id: str #: The parameter bindings in scope for this graph frame (a loop body frame's #: scope is the parent's params merged with the current row). - params: dict[str, str] = Field(default_factory=dict) + params: dict[str, RuntimeParamScalar] = Field(default_factory=dict) #: Present iff this frame is a loop-body iteration -- the loop's cursor. loop: Optional[LoopCursor] = None @@ -156,7 +158,7 @@ def control_frames_hash(frames: list[GraphFrame]) -> str: return "sha256:" + hashlib.sha256(canonical).hexdigest() -def bound_params_sha256(params: dict[str, str]) -> str: +def bound_params_sha256(params: Mapping[str, RuntimeParamScalar]) -> str: """Return a PHI-free digest of one exact attended parameter scope.""" canonical = json.dumps( @@ -227,7 +229,7 @@ class ProgramCheckpoint(BaseModel): #: :class:`GraphFrame`). ``frames[-1]`` is the leaf (the verified state). frames: list[GraphFrame] = Field(min_length=1) #: The parameter bindings in scope at the leaf (resume re-binds these). - bound_params: dict[str, str] = Field(default_factory=dict) + bound_params: dict[str, RuntimeParamScalar] = Field(default_factory=dict) #: Contract hashes (``Effect.contract_hash``) of the effects CONFIRMED AT #: THIS state -- appended to the run's completed-effect ledger. Union across #: all checkpoints = every already-performed consequential write, so a resume diff --git a/openadapt_flow/runtime/durable/resume.py b/openadapt_flow/runtime/durable/resume.py index aaaa1cd9..35ee8bb2 100644 --- a/openadapt_flow/runtime/durable/resume.py +++ b/openadapt_flow/runtime/durable/resume.py @@ -30,6 +30,7 @@ from __future__ import annotations +from collections.abc import Mapping from datetime import datetime from pathlib import Path from types import SimpleNamespace @@ -37,6 +38,10 @@ from openadapt_flow.ir import ExecutionTargetKind, RunReport, Step, Workflow from openadapt_flow.policy import effects_for_actuation +from openadapt_flow.runtime.authorization import ( + RuntimeParamScalar, + runtime_params_for_gui, +) from openadapt_flow.runtime.durable.approval import ( ApprovalRecord, ApprovalRequired, @@ -62,13 +67,13 @@ def _resolved_step_effects( step: Step, *, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], run_id: str, actuation: Optional[str], ) -> list[Effect]: """Resolve the exact path-specific effects declared by one retained step.""" - namespace = {**params, "__run_id__": run_id} + namespace = {**runtime_params_for_gui(params), "__run_id__": run_id} return [ effect.resolve(namespace) for effect in effects_for_actuation(step, actuation) ] @@ -77,7 +82,7 @@ def _resolved_step_effects( def _validate_retained_step_proof( *, step: Step, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], run_id: str, skipped: bool, actuation: Optional[str], @@ -1103,7 +1108,7 @@ def _resume_program( checkpoint: Optional[ProgramCheckpoint], checkpoints: list[ProgramCheckpoint], bundle_dir: Path, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], worklists: dict[str, list[dict[str, str]]], save_healed_to: Optional[Path | str], live_bundle_version: str, diff --git a/openadapt_flow/runtime/effects/adapter.py b/openadapt_flow/runtime/effects/adapter.py index 559e7de1..212824dd 100644 --- a/openadapt_flow/runtime/effects/adapter.py +++ b/openadapt_flow/runtime/effects/adapter.py @@ -118,7 +118,7 @@ #: run parameters for ``ValueExpr({param: ...})`` binding. A factory must fail #: LOUD (raise ``ValueError``) on missing config or secrets -- never construct #: a silently broken verifier. -VerifierFactory = Callable[[Any, Optional[Mapping[str, str]]], Any] +VerifierFactory = Callable[[Any, Optional[Mapping[str, object]]], Any] class ConnectionProbe(BaseModel): diff --git a/openadapt_flow/runtime/effects/effect.py b/openadapt_flow/runtime/effects/effect.py index 87bd6abe..908bca6f 100644 --- a/openadapt_flow/runtime/effects/effect.py +++ b/openadapt_flow/runtime/effects/effect.py @@ -93,7 +93,7 @@ def _exactly_one_source(self) -> "ValueExpr": raise ValueError("effect parameter name must not be empty") return self - def resolve(self, params: Mapping[str, str]) -> Optional[str]: + def resolve(self, params: Mapping[str, object]) -> Optional[str]: """Resolve to a concrete string against ``params``. A ``param`` reference reads ``params[param]`` (``None`` when the run did @@ -102,10 +102,16 @@ def resolve(self, params: Mapping[str, str]) -> Optional[str]: record). A pure literal returns its literal unchanged. """ if self.param is not None: - return params.get(self.param) + if self.param not in params: + return None + # Keep the authorized JSON scalar typed until this exact + # string-only effect-verifier boundary. + from openadapt_flow.runtime.authorization import runtime_param_text + + return runtime_param_text(params[self.param]) # type: ignore[arg-type] return self.literal - def resolved(self, params: Mapping[str, str]) -> "ValueExpr": + def resolved(self, params: Mapping[str, object]) -> "ValueExpr": """Return a pure-literal copy of this expression bound to ``params``.""" return ValueExpr(literal=self.resolve(params)) @@ -482,7 +488,7 @@ def requires_baseline(self) -> bool: # -- run-time parameter binding (P0-3) ----------------------------------- def resolve( self, - params: Mapping[str, str], + params: Mapping[str, object], *, opaque_param_sha256: Mapping[str, str] | None = None, ) -> "Effect": @@ -519,7 +525,7 @@ def resolve( def resolved_contract_hash( self, - params: Mapping[str, str], + params: Mapping[str, object], *, opaque_param_sha256: Mapping[str, str] | None = None, ) -> str: @@ -538,14 +544,14 @@ def resolved_contract_hash( char not in "0123456789abcdef" for char in digest ): raise ValueError(f"opaque parameter {name!r} has an invalid digest") - if ( - name in params - and hashlib.sha256(str(params[name]).encode("utf-8")).hexdigest() - != digest - ): - raise ValueError( - f"opaque parameter {name!r} digest does not match its value" - ) + if name in params: + resolved_value = ValueExpr(param=name).resolve(params) + if resolved_value is None or ( + hashlib.sha256(resolved_value.encode("utf-8")).hexdigest() != digest + ): + raise ValueError( + f"opaque parameter {name!r} digest does not match its value" + ) missing = self.referenced_params().difference(params).difference(opaque) if missing: raise ValueError("effect contract references an unavailable parameter") @@ -558,7 +564,7 @@ def value(expr: ValueExpr | None) -> object: "opaque_param": expr.param, "sha256": opaque[expr.param], } - return str(expr.resolved(params)) + return expr.resolve(params) payload: dict[str, object] = { "kind": self.kind.value, diff --git a/openadapt_flow/runtime/program_predicates.py b/openadapt_flow/runtime/program_predicates.py index 6707e97c..8ba9452f 100644 --- a/openadapt_flow/runtime/program_predicates.py +++ b/openadapt_flow/runtime/program_predicates.py @@ -269,7 +269,7 @@ def predicate_uses_frame(predicate: Predicate | None) -> bool: def evaluate_program_predicate( predicate: Predicate, frame_png: bytes, - params: Mapping[str, str], + params: Mapping[str, object], *, vision: Any, viewport: tuple[int, int] | None, @@ -308,9 +308,11 @@ def evaluate_program_predicate( if kind is PredicateKind.TEXT_ABSENT: return not (predicate.text and vision.text_present(frame_png, predicate.text)) if kind is PredicateKind.PARAM_EQUALS: - return predicate.param is not None and str(params.get(predicate.param)) == str( - predicate.value - ) + if predicate.param is None or predicate.param not in params: + return False + from openadapt_flow.runtime.authorization import runtime_param_text + + return runtime_param_text(params[predicate.param]) == predicate.value # type: ignore[arg-type] if kind is PredicateKind.AND: return all( evaluate_program_predicate( diff --git a/openadapt_flow/runtime/replayer.py b/openadapt_flow/runtime/replayer.py index 53f64903..91928d6f 100644 --- a/openadapt_flow/runtime/replayer.py +++ b/openadapt_flow/runtime/replayer.py @@ -46,7 +46,16 @@ from copy import deepcopy from datetime import date, datetime, timezone from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, TypeVar, cast +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Literal, + Mapping, + Optional, + TypeVar, + cast, +) from urllib.parse import urlsplit from openadapt_flow.backend import ( @@ -135,7 +144,10 @@ from openadapt_flow.runtime import identity as identity_mod from openadapt_flow.runtime.authorization import ( GovernedRunAuthorization, + RuntimeParamScalar, runtime_inputs_digest, + runtime_param_text, + runtime_params_for_gui, ) from openadapt_flow.runtime.durable.approval import StateDiverged from openadapt_flow.runtime.durable.program_checkpoint import ( @@ -314,7 +326,7 @@ def __init__(self, outcome: str, reason: str, *, safety: bool = False) -> None: # Empty means the halt is not eligible for a generic attended # Continue/Skip transition. self.program_frames: list[GraphFrame] = [] - self.program_params: dict[str, str] = {} + self.program_params: dict[str, RuntimeParamScalar] = {} self.program_history_hash: str = "" @@ -598,7 +610,7 @@ def __init__( self._governed_asset_hashes: dict[str, str] = {} self._governed_plaintext_assets = False self._governed_asset_mutation: Optional[str] = None - self._governed_base_params: Optional[dict[str, str]] = None + self._governed_base_params: Optional[dict[str, RuntimeParamScalar]] = None self._active_runtime_worklists: Optional[dict[str, list[dict[str, str]]]] = None self._active_delivery_resolution: Optional[Resolution] = None self._active_delivery_region: Optional[Region] = None @@ -751,7 +763,7 @@ def _durable_resume_payload( run_dir: Path, bundle_dir: Path, run_id: str, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], worklists: dict[str, list[dict[str, str]]], resume_from: Optional[int], resume_program: Optional[ProgramCheckpoint], @@ -848,7 +860,7 @@ def _admit_durable_resume( run_dir: Path, bundle_dir: Path, run_id: Optional[str], - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], worklists: dict[str, list[dict[str, str]]], resume_from: Optional[int], resume_program: Optional[ProgramCheckpoint], @@ -913,7 +925,7 @@ def _consume_durable_resume_admission( run_dir: Path, bundle_dir: Path, run_id: str, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], worklists: dict[str, list[dict[str, str]]], resume_from: Optional[int], resume_program: Optional[ProgramCheckpoint], @@ -983,7 +995,7 @@ def run( self, workflow: Workflow, *, - params: Optional[dict[str, str]] = None, + params: Optional[dict[str, RuntimeParamScalar]] = None, worklists: Optional[dict[str, list[dict[str, str]]]] = None, bundle_dir: Path, run_dir: Path, @@ -1134,7 +1146,7 @@ def run( # with caller-supplied values overriding both. A v0 bundle (empty # ``param_specs``) collapses to exactly the old ``{**workflow.params, # **caller}`` merge. - merged: dict[str, str] = {**workflow.params} + merged: dict[str, RuntimeParamScalar] = {**workflow.params} for pname, spec in workflow.param_specs.items(): if spec.example is not None: merged.setdefault(pname, spec.example) @@ -1543,7 +1555,11 @@ def acknowledge_delivery(self_nonlocal) -> None: missing = sorted( pname for pname, spec in workflow.param_specs.items() - if spec.required and not params.get(pname) + if spec.required + and ( + pname not in params + or (isinstance(params[pname], str) and params[pname] == "") + ) ) if missing: report.results.append( @@ -2295,7 +2311,7 @@ def _interpret_program( self, workflow: Workflow, *, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], worklists: dict[str, list[dict[str, str]]], bundle_dir: Path, run_dir: Path, @@ -2334,7 +2350,7 @@ def _interpret_program( # Where the interpreter currently is (for a durable pause record). self._current_state_id: str = "" self._current_intent: str = "" - self._current_params: dict[str, str] = dict(params) + self._current_params: dict[str, RuntimeParamScalar] = dict(params) if durable_run is not None: self._bundle_version = _bundle_version(bundle_dir) if self._durable_resume_mode == "program": @@ -2532,7 +2548,7 @@ def _walk_graph( *, graph_id: str, workflow: Workflow, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], worklists: dict[str, list[dict[str, str]]], bundle_dir: Path, run_dir: Path, @@ -2591,7 +2607,7 @@ def _run_states_from( frame: dict, *, workflow: Workflow, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], worklists: dict[str, list[dict[str, str]]], bundle_dir: Path, run_dir: Path, @@ -2632,7 +2648,7 @@ def _run_states_from( state.decision.question if state.decision is not None else state.id ) ) - self._current_params = params + self._current_params = dict(params) if state.kind is StateKind.TERMINAL: self._raise_on_governed_asset_mutation() @@ -2668,7 +2684,7 @@ def _exec_state( graph: ProgramGraph, *, workflow: Workflow, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], worklists: dict[str, list[dict[str, str]]], bundle_dir: Path, run_dir: Path, @@ -2780,7 +2796,7 @@ def _exec_action_state( graph: ProgramGraph, *, workflow: Workflow, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], bundle_dir: Path, run_dir: Path, report: RunReport, @@ -2927,7 +2943,7 @@ def _exec_loop_state( state: State, *, workflow: Workflow, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], worklists: dict[str, list[dict[str, str]]], bundle_dir: Path, run_dir: Path, @@ -3058,7 +3074,7 @@ def _exec_business_decision_state( state: State, *, workflow: Workflow, - params: dict[str, str], + params: dict[str, RuntimeParamScalar], bundle_dir: Path, report: RunReport, run_dir: Path, @@ -3212,7 +3228,7 @@ def _select_transition( state: State, *, workflow: Workflow, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], bundle_dir: Path, report: Optional[RunReport] = None, run_dir: Optional[Path] = None, @@ -3566,14 +3582,14 @@ def _program_scope_from_frames( def _params_with_business_decisions( self, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], scope: list[ProgramExecutionScopeFrame], evidence: list[BusinessDecisionEvidence], *, run_dir: Path, workflow: Workflow, governed_runtime_inputs_digest: str | None, - ) -> dict[str, str]: + ) -> dict[str, RuntimeParamScalar]: """Reapply authenticated decisions made in this exact frame scope.""" if not evidence: @@ -3600,7 +3616,10 @@ def _params_with_business_decisions( return resolved def _skip_completed_effect_state( - self, state: State, params: dict[str, str], report: RunReport + self, + state: State, + params: Mapping[str, RuntimeParamScalar], + report: RunReport, ) -> bool: """Idempotency guard: skip an action state whose declared effects were ALL already CONFIRMED (in the completed-effect ledger). @@ -3797,7 +3816,7 @@ def _record_program_checkpoint( self, state: State, result: StepResult, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], report: RunReport, ) -> None: """Persist a verified-state interpreter checkpoint (Tier-3, program mode). @@ -4342,7 +4361,7 @@ def revalidate_attended_program_completion( *, graph_id: str, state_id: str, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], bundle_dir: Path, run_dir: Path, run_id: str, @@ -4418,7 +4437,7 @@ def select_attended_program_transition( *, graph_id: str, state_id: str, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], bundle_dir: Path, ) -> Optional[str]: """Select and prove one exact successor for an attended action state.""" @@ -4488,7 +4507,7 @@ def revalidate_attended_completion( workflow: Workflow, *, step_index: int, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], bundle_dir: Path, run_dir: Path, run_id: str, @@ -5495,7 +5514,7 @@ def _run_step( *, workflow: Workflow, step_index: int, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], bundle_dir: Path, run_dir: Path, new_crops: dict[str, bytes], @@ -6363,7 +6382,7 @@ def _run_step( selection_error: Optional[str] = None if step.action is ActionKind.SELECT_OPTION: selection_text = ( - params[step.param] + runtime_param_text(params[step.param]) if step.param is not None else step.text or "" ) @@ -6670,7 +6689,7 @@ def _api_request_pointer_field_path(pointer: str) -> tuple[str, ...]: def _api_identity_refusal( self, step: Step, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], workflow: Workflow, effects: list["Effect"], result: StepResult, @@ -6709,7 +6728,8 @@ def _api_identity_refusal( f"({step.intent}): semantic signal {identity.key!r} is not " "part of the qualified identity policy" ) - value = params.get(identity.param) + raw_value = params.get(identity.param) + value = runtime_param_text(raw_value) if raw_value is not None else None if value is None or value == "": return ( f"API identity verification HALTED step '{step.id}' " @@ -6782,7 +6802,7 @@ def _api_identity_refusal( def _try_api_tier( self, step: Step, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], result: StepResult, *, workflow: Workflow, @@ -6988,7 +7008,10 @@ def _try_api_tier( result.delivery_attempted = True try: self._require_qualification_environment_current() - outcome = self.api_actuator.actuate(binding, params) + outcome = self.api_actuator.actuate( + binding, + runtime_params_for_gui(params), + ) except Exception as exc: # noqa: BLE001 - external actuator boundary # The actuator contract is no-throw, but a deployment adapter can # still violate it. The delivery boundary was crossed immediately @@ -7332,7 +7355,7 @@ def _set_api_unavailable_refusal( # -- system-of-record effect verification ----------------------------------- def _resolve_effects( - self, effects: list["Effect"], params: dict[str, str] + self, effects: list["Effect"], params: Mapping[str, RuntimeParamScalar] ) -> list["Effect"]: """Bind each effect's ``ValueExpr`` contract to THIS run's params (P0-3). @@ -7343,7 +7366,10 @@ def _resolve_effects( idempotency key can be bound per-run. A pure-literal (v1) effect is returned value-identical -- ``resolve`` is a no-op for it. """ - namespace = {**params, "__run_id__": self._run_id} + namespace = { + **{name: runtime_param_text(value) for name, value in params.items()}, + "__run_id__": self._run_id, + } run_id_sha256 = hashlib.sha256(self._run_id.encode("utf-8")).hexdigest() return [ effect.resolve( @@ -7810,7 +7836,7 @@ def _identity_gate_error( step: Step, resolution: Resolution, frame_png: bytes, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], workflow: Workflow, bundle_dir: Path, result: StepResult, @@ -8391,7 +8417,7 @@ def _revalidate_consequential_actuation( resolution: Optional[Resolution], matched_region: Optional[Region], before_png: bytes, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], workflow: Workflow, bundle_dir: Path, result: StepResult, @@ -8982,9 +9008,9 @@ def _fresh_actuation_event( def _active_program_frame_refusal( self, workflow: Workflow, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], step: Step, - base_params: dict[str, str], + base_params: Mapping[str, RuntimeParamScalar], ) -> Optional[str]: """Bind the live interpreter path to the sealed program before input.""" @@ -9103,7 +9129,7 @@ def _active_program_frame_refusal( def _fresh_actuation_authorization_refusal( self, workflow: Workflow, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], step: Step, ) -> Optional[str]: """Recheck exact governed authority and inputs before fresh input.""" @@ -9336,7 +9362,7 @@ def _qualification_campaign_refusal(self, workflow: Workflow) -> Optional[str]: def _delivery_authorization_refusal( self, workflow: Workflow, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], step: Step, result: StepResult, ) -> Optional[str]: @@ -9400,7 +9426,7 @@ def _act( self, step: Step, resolution: Optional[Resolution], - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], *, workflow: Workflow, step_index: int, @@ -9888,7 +9914,7 @@ def _act( f"Step '{step.id}' ({step.intent}) requires parameter " f"'{step.param}' but it was not provided" ) - text = params[step.param] + text = runtime_param_text(params[step.param]) elif step.text is not None: text = step.text else: @@ -10677,7 +10703,7 @@ def _handle_interstitials( step: Step, before_png: bytes, bundle_dir: Path, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], result: StepResult, audit_events: list[InterstitialActionResult], workflow: Optional[Workflow], @@ -11003,7 +11029,7 @@ def _apply_step_gates( step: Step, before_png: bytes, bundle_dir: Path, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], result: StepResult, workflow: Optional[Workflow] = None, ) -> tuple[bool, Optional[str], bytes]: @@ -11135,7 +11161,7 @@ def _predicate_holds( pred: Predicate, frame_png: bytes, bundle_dir: Path, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], *, workflow: Optional[Workflow] = None, ) -> bool: @@ -11150,7 +11176,7 @@ def _predicate_holds( return evaluate_program_predicate( pred, frame_png, - params, + runtime_params_for_gui(params), vision=self.vision, viewport=self.backend.viewport, asset_loader=lambda rel: self._asset_bytes( @@ -11220,7 +11246,7 @@ def _compare_qualified_signal_text( signal: Any, anchor: Anchor, live: Optional[str], - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], workflow: Workflow, ) -> Literal["verified", "conflict", "unverifiable"]: """Compare one live source without returning or logging identity values.""" @@ -11259,7 +11285,7 @@ def _compare_qualified_signal_text( ) if set(parameter_names) != set(signal.params): return "unverifiable" - live_values = {**workflow.params, **params} + live_values = {**workflow.params, **runtime_params_for_gui(params)} live_form, used = parameterize_identity_text( live, live_values, @@ -11293,7 +11319,7 @@ def _compare_qualified_signal_text( match=signal.match.value, normalizers=signal.normalizers, live=live, - params=params, + params=runtime_params_for_gui(params), param_examples=workflow.params, parameter_names=signal.params, extract_pattern=extract_pattern, @@ -11406,7 +11432,7 @@ def _verify_signal_quorum( step: Step, resolution: Resolution, before_png: bytes, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], workflow: Workflow, bundle_dir: Optional[Path], policy: Any, @@ -11519,7 +11545,10 @@ def _verify_signal_quorum( # pixels than the demonstration. Exact/explicitly normalized # OCR may verify it only when the expected value does not # occupy the known glyph-confusable identifier class. - live_values = {**workflow.params, **params} + live_values = { + **workflow.params, + **runtime_params_for_gui(params), + } vulnerable = any( identity_mod.identity_rests_on_confusable_identifier( live_values.get(name) @@ -11571,7 +11600,7 @@ def _compare_direct_signal_text( recorded: Optional[str], live: Optional[str], *, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], workflow: Workflow, ) -> tuple[ Literal["verified", "conflict", "unverifiable"], @@ -11590,7 +11619,7 @@ def _compare_direct_signal_text( return "unverifiable", signal.params live, used = parameterize_identity_text( live, - {**workflow.params, **params}, + {**workflow.params, **runtime_params_for_gui(params)}, names=parameter_names, minimum_chars=identity_mod.MIN_PARAM_CHARS, case_sensitive=not ( @@ -11614,7 +11643,7 @@ def _verify_identity( step: Step, resolution: Resolution, before_png: bytes, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], workflow: Workflow, bundle_dir: Optional[Path] = None, ) -> IdentityCheck: @@ -11668,7 +11697,7 @@ def _verify_identity( step=step, resolution=resolution, before_png=before_png, - params=params, + params=runtime_params_for_gui(params), workflow=workflow, bundle_dir=bundle_dir, policy=identity_policy, @@ -11696,7 +11725,7 @@ def structured_tier() -> Optional[IdentityCheck]: return itmpl.verify_structured_template( tmpl, live, - params=params, + params=runtime_params_for_gui(params), param_examples=workflow.params, ) return identity_mod.verify_structured_identity(recorded, live) @@ -11802,7 +11831,7 @@ def _verify_identity_ocr( step: Step, resolution: Resolution, before_png: bytes, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], workflow: Workflow, ) -> IdentityCheck: """OCR name+DOB-primary identity tier (the pixel-substrate fallback). @@ -11902,7 +11931,7 @@ def attempt( return itmpl.verify_template_identity( anchor.identity_template, observed, - params=params, + params=runtime_params_for_gui(params), param_examples=workflow.params, ) # This branch means no identity_template, so the constructor-time @@ -11912,7 +11941,7 @@ def attempt( return identity_mod.verify_target_identity( anchor.context_text, observed, - params=params, + params=runtime_params_for_gui(params), param_examples=workflow.params, ) @@ -12135,7 +12164,7 @@ def _verify_typed_input( result: StepResult, *, workflow: Workflow, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], bundle_dir: Path, run_dir: Path, resolution: Optional[Resolution], @@ -12525,7 +12554,7 @@ def _implicit_scroll_target_ready( *, workflow: Workflow, bundle_dir: Path, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], ) -> bool: """Resolve an implicit scroll target and honor its armed identity. @@ -12595,7 +12624,7 @@ def _act_scroll( bundle_dir: Path, run_dir: Path, before_png: bytes, - params: dict[str, str], + params: Mapping[str, RuntimeParamScalar], result: StepResult, graph_ctx: Optional["_GraphStepContext"] = None, ) -> Optional[str]: diff --git a/openadapt_flow/visualize/builder.py b/openadapt_flow/visualize/builder.py index 5608d968..be45cdff 100644 --- a/openadapt_flow/visualize/builder.py +++ b/openadapt_flow/visualize/builder.py @@ -14,6 +14,7 @@ import re from typing import TYPE_CHECKING, Callable, Optional +from openadapt_flow.runtime.authorization import runtime_param_text from openadapt_flow.visualize.spec import ( BundleMeta, EdgeKind, @@ -293,7 +294,11 @@ def _bundle_meta( type=spec.type.value, required=spec.required, secret=name in (workflow.secret_params or []), - example=spec.example, + example=( + runtime_param_text(spec.example) + if spec.example is not None + else None + ), choices=list(spec.choices), ) ) diff --git a/pyproject.toml b/pyproject.toml index f1e5871e..faa60490 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "openadapt-flow" -version = "1.33.0" +version = "1.34.0" description = "Compile demonstrated GUI workflows into deterministic local replay with governed repair and refusal" readme = "README.md" license = { text = "MIT" } diff --git a/tests/test_effect_kit_config.py b/tests/test_effect_kit_config.py index 221b1623..4a43a4bd 100644 --- a/tests/test_effect_kit_config.py +++ b/tests/test_effect_kit_config.py @@ -76,6 +76,25 @@ def test_path_params_bind_run_params_url_quoted(self, monkeypatch): assert "applicant=OpenAdapt%20Synthetic" in v.records_path assert v.headers == {"Authorization": "Bearer tok"} + def test_path_params_render_typed_scalars_with_json_text(self): + cfg = EffectsConfig( + kind="rest", + base_url="http://sor.local", + records_path="/x?enabled={enabled}&count={count}&ratio={ratio}", + path_params={ + "enabled": {"param": "enabled"}, + "count": {"param": "count"}, + "ratio": {"param": "ratio"}, + }, + ) + + verifier = build_effect_verifier( + cfg, + params={"enabled": False, "count": 0, "ratio": 1e-7}, + ) + + assert verifier.records_path == "/x?enabled=false&count=0&ratio=1e-7" + def test_auth_headers_sent_on_reads_only_when_configured(self): class _Session: def __init__(self): diff --git a/tests/test_execution_profiles.py b/tests/test_execution_profiles.py index 827f5e35..0a2da9cd 100644 --- a/tests/test_execution_profiles.py +++ b/tests/test_execution_profiles.py @@ -69,7 +69,9 @@ from openadapt_flow.runner.evidence import summary_status from openadapt_flow.runtime.authorization import ( GovernedRunAuthorization, + RuntimeParamScalar, runtime_inputs_digest, + runtime_param_text, ) from openadapt_flow.runtime.durable import ApprovalRequired, CheckpointStore, resume from openadapt_flow.runtime.effects import ( @@ -1523,21 +1525,26 @@ def _workflow(transitions: list[Transition]) -> Workflow: ), ) - def _report(workflow: Workflow, *, route: str) -> RunReport: + def _report( + workflow: Workflow, + *, + route: RuntimeParamScalar, + selected_state: str = "second", + ) -> RunReport: report = RunReport( workflow_name=workflow.name, started_at="2026-07-28T00:00:00Z", success=True, execution_completed=True, terminal_outcome="success", - visited_states=["pick", "second", "done"], + visited_states=["pick", selected_state, "done"], params={"route": route}, governed_authorization_id="authorization-1", governed_runtime_inputs_digest="b" * 64, results=[ StepResult( - step_id="second", - intent="second", + step_id=selected_state, + intent=selected_state, ok=True, starting_state_settled=True, delivery_attempted=True, @@ -1550,7 +1557,9 @@ def _report(workflow: Workflow, *, route: str) -> RunReport: _bind_report_to_workflow(report, workflow) pick = workflow.program.states["pick"] first_guard_matches = bool( - pick.transitions[0].guard is not None and route == "first" + pick.transitions[0].guard is not None + and pick.transitions[0].guard.value is not None + and runtime_param_text(route) == pick.transitions[0].guard.value ) report.program_transition_evidence = [ *_transition_evidence( @@ -1562,7 +1571,7 @@ def _report(workflow: Workflow, *, route: str) -> RunReport: ), *_transition_evidence( decision_index=1, - state=workflow.program.states["second"], + state=workflow.program.states[selected_state], verdicts=[True], target="done", inputs_digest=report.governed_runtime_inputs_digest or "", @@ -1611,6 +1620,34 @@ def _report(workflow: Workflow, *, route: str) -> RunReport: ) is ExecutionOutcome.VERIFIED ) + for value, expected_text in [ + (False, "false"), + (0, "0"), + (1e-7, "1e-7"), + (1e20, "100000000000000000000"), + (1e21, "1e+21"), + ]: + typed_guard = _workflow( + [ + Transition( + guard=Predicate( + kind=PredicateKind.PARAM_EQUALS, + param="route", + value=expected_text, + ), + target="first", + ), + Transition(target="second"), + ] + ) + assert ( + classify_execution_outcome( + _report(typed_guard, route=value, selected_state="first"), + typed_guard, + ExecutionProfile.STANDARD, + ) + is ExecutionOutcome.VERIFIED + ) exact = _report(ordered_guard, route="second") for update in ( {"graph_id": "forged-graph"}, diff --git a/tests/test_governed_authorization.py b/tests/test_governed_authorization.py index e6879534..bd500352 100644 --- a/tests/test_governed_authorization.py +++ b/tests/test_governed_authorization.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import json import pytest @@ -28,6 +29,7 @@ from openadapt_flow.runtime.authorization import ( GovernedRunAuthorization, UnverifiedWriteApproval, + runtime_inputs_bytes, runtime_inputs_digest, ) from openadapt_flow.runtime.durable import ( @@ -119,6 +121,60 @@ def _authorization( ) +def test_runtime_inputs_match_hosted_javascript_scalar_vector() -> None: + workflow = Workflow(name="cross-language-scalars") + params = { + "whole": 7, + "float": 1.5, + "small": 1e-7, + "mid": 1e20, + "large": 1e21, + "bool_true": True, + "bool_false": False, + "string_int": "7", + "string_float": "1.5", + "string_bool": "true", + } + expected = ( + b'{"params":{"bool_false":false,"bool_true":true,"float":1.5,' + b'"large":1e+21,"mid":100000000000000000000,"small":1e-7,' + b'"string_bool":"true",' + b'"string_float":"1.5","string_int":"7","whole":7},' + b'"worklists":{}}' + ) + + actual = runtime_inputs_bytes(workflow, params, None) + + assert actual == expected + assert hashlib.sha256(actual).hexdigest() == ( + "1f9cf0ab74f3194d4759be349239e6f7bbeef13f1f4641bd9e4e55b018728293" + ) + + +def test_runtime_inputs_sort_object_keys_as_javascript_utf16() -> None: + workflow = Workflow(name="cross-language-unicode-order") + params = {"\ue000": 1, "\U0001f600": 2} + expected = '{"params":{"😀":2,"":1},"worklists":{}}'.encode() + + actual = runtime_inputs_bytes(workflow, params, None) + + assert actual == expected + assert hashlib.sha256(actual).hexdigest() == ( + "f70fa507dff3b9f0d54ab13325e5588a18b718f697d3e4a7877eadf06dcc7196" + ) + assert runtime_inputs_digest(workflow, params, None) != runtime_inputs_digest( + workflow, + {name: str(value) for name, value in params.items()}, + None, + ) + + +@pytest.mark.parametrize("value", [9_007_199_254_740_993, float("nan"), float("inf")]) +def test_runtime_inputs_refuse_non_interoperable_numbers(value: object) -> None: + with pytest.raises(ValueError, match="runtime parameters"): + runtime_inputs_bytes(Workflow(name="unsafe-number"), {"value": value}, None) + + def test_in_memory_semantic_mutation_halts_before_action(tmp_path): step = context_click_step("Jane Sample 1980-01-15 MRN 123") workflow, bundle = _seal(tmp_path, Workflow(name="semantic", steps=[step])) diff --git a/tests/test_hosted_runner_adapter.py b/tests/test_hosted_runner_adapter.py index 40146033..1f8b6e08 100644 --- a/tests/test_hosted_runner_adapter.py +++ b/tests/test_hosted_runner_adapter.py @@ -3,25 +3,35 @@ import hashlib import json import os -from base64 import b64encode, urlsafe_b64encode +from base64 import b64decode, b64encode, urlsafe_b64encode from dataclasses import replace from datetime import datetime, timezone from pathlib import Path +from types import SimpleNamespace import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey import openadapt_flow.runner.hosted_adapter as hosted -from openadapt_flow.ir import ParamKind, ParamSpec +from openadapt_flow.__main__ import _replay_params +from openadapt_flow.ir import ParamKind, ParamSpec, Workflow from openadapt_flow.runner.hosted_adapter import ( + RUNNER_RENEWAL_HEADER, AdmissionArtifactBytes, + CallbackRequest, DeliveryAuthority, HostedDispatch, HostedRunnerAdapter, + HostedRunResult, + HostedTerminalEvent, ManagedExecution, + PollRequest, RegisterCapabilities, + RegisterRequest, + registration_renewal_headers, ) +from openadapt_flow.runner.inputs import resolve_admitted_params from openadapt_flow.runner.product_release import ( DOMAIN, TARGETS, @@ -33,13 +43,30 @@ ) from openadapt_flow.runner.protocol import ( DispatchParamsRef, + DispatchParamsValues, RunnerDispatchPayload, dispatch_binding_sha256, ) from openadapt_flow.runner.verify import VerifiedDispatch +from openadapt_flow.runtime.authorization import ( + runtime_inputs_bytes, + runtime_param_text, +) from openadapt_flow.runtime.durable.authority import REMOTE_DISPATCH_SESSION_ID_ENV +from openadapt_flow.terminal_verification_v2 import ( + ProductionDeliveryPermit, + ProductionDeliveryPermitChain, + ProductionDeliveryPermitPayload, + ProductionDeliveryReceiptPayload, + evidence_runner_signer_sha256, + sign_production_delivery_permit, + sign_production_delivery_receipt, + sign_production_terminal_verification, +) from openadapt_flow.transaction import TransactionOutcome +from tests.test_run_receipt import _report as _production_report from tests.test_runner_client_lib import dispatch_payload +from tests.test_terminal_verification_v2 import _payload, _private_key pytest_plugins = ("tests.test_runner_client_lib",) @@ -220,6 +247,151 @@ def test_registration_refuses_without_protected_runner_origin( ) +def test_protected_runner_origin_is_public_strict_accessor( + monkeypatch, tmp_path, config +) -> None: + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite") + local_config = replace(config, host="https://cloud.example") + monkeypatch.setattr( + hosted, "load_runner_config", lambda *_args, **_kwargs: local_config + ) + + assert adapter.protected_runner_origin(tmp_path / "runner.toml") == ( + "https://cloud.example" + ) + + +def test_runner_renewal_token_has_one_register_only_header_boundary() -> None: + token = "oar_" + "a" * 64 + + assert registration_renewal_headers(None) == {} + assert registration_renewal_headers("") == {} + assert registration_renewal_headers(token) == { + RUNNER_RENEWAL_HEADER: token, + } + for invalid in ("runner-token", "oar_" + "A" * 64, "oar_" + "a" * 63): + with pytest.raises(ValueError, match="renewal credential"): + registration_renewal_headers(invalid) + + for request_type in (RegisterRequest, PollRequest, HostedDispatch, CallbackRequest): + assert "runner_token" not in request_type.model_fields + assert RUNNER_RENEWAL_HEADER not in request_type.model_fields + assert all("renewal" not in name for name in request_type.model_fields) + + +def test_dispatch_param_scalars_round_trip_without_coercion() -> None: + values = { + "text": "1.5", + "enabled": True, + "count": 7, + "ratio": 1.5, + "small": 1e-7, + "large": 1e21, + } + + parsed = DispatchParamsValues.model_validate({"values": values}) + + assert parsed.model_dump(mode="json")["values"] == values + assert type(parsed.values["enabled"]) is bool + assert type(parsed.values["count"]) is int + assert type(parsed.values["ratio"]) is float + + +@pytest.mark.parametrize( + "value", + [None, {}, [], float("nan"), float("inf"), 9_007_199_254_740_993], +) +def test_dispatch_param_scalars_refuse_invalid_values(value: object) -> None: + with pytest.raises(ValueError): + DispatchParamsValues.model_validate({"values": {"value": value}}) + + +@pytest.mark.parametrize( + "name", + ["\ue000", "\U0001f600", "has-dash", "a" * 129], +) +def test_dispatch_param_names_use_shared_ascii_grammar(name: str) -> None: + with pytest.raises(ValueError, match="parameter name"): + DispatchParamsValues.model_validate({"values": {name: "value"}}) + + +def test_private_params_file_preserves_scalar_types(tmp_path) -> None: + values = {"text": "false", "enabled": False, "count": 0, "ratio": 1.5} + + path = HostedRunnerAdapter._write_params(tmp_path / "params.json", values) + + assert path is not None + assert json.loads(path.read_bytes()) == values + if os.name != "nt": + assert path.stat().st_mode & 0o777 == 0o600 + + +def test_scalar_dispatch_to_gui_boundary_preserves_exact_types(tmp_path) -> None: + values = { + "string_int": "7", + "string_float": "1.5", + "string_bool": "true", + "bool_true": True, + "bool_false": False, + "whole": 7, + "float": 1.5, + "small": 1e-7, + "mid": 1e20, + "large": 1e21, + } + kinds = { + "string_int": ParamKind.STRING, + "string_float": ParamKind.STRING, + "string_bool": ParamKind.STRING, + "bool_true": ParamKind.BOOLEAN, + "bool_false": ParamKind.BOOLEAN, + "whole": ParamKind.NUMBER, + "float": ParamKind.NUMBER, + "small": ParamKind.NUMBER, + "mid": ParamKind.NUMBER, + "large": ParamKind.NUMBER, + } + workflow = Workflow( + name="scalar-path", + param_specs={ + name: ParamSpec(name=name, type=kind, required=True) + for name, kind in kinds.items() + }, + ) + + wire = DispatchParamsValues.model_validate({"values": values}) + admitted = resolve_admitted_params(workflow, dict(wire.values), inline=True) + expected = ( + b'{"params":{"bool_false":false,"bool_true":true,"float":1.5,' + b'"large":1e+21,"mid":100000000000000000000,"small":1e-7,' + b'"string_bool":"true","string_float":"1.5","string_int":"7",' + b'"whole":7},"worklists":{}}' + ) + params_path = HostedRunnerAdapter._write_params(tmp_path / "params.json", admitted) + assert params_path is not None + child_params = _replay_params(None, str(params_path)) + + assert runtime_inputs_bytes(workflow, admitted, None) == expected + assert child_params == values + assert {name: type(value) for name, value in child_params.items()} == { + name: type(value) for name, value in values.items() + } + assert { + name: runtime_param_text(value) for name, value in child_params.items() + } == { + "string_int": "7", + "string_float": "1.5", + "string_bool": "true", + "bool_true": "true", + "bool_false": "false", + "whole": "7", + "float": "1.5", + "small": "1e-7", + "mid": "100000000000000000000", + "large": "1e+21", + } + + def _prepared_adapter(monkeypatch, tmp_path, config, workflow, runner): config = replace(config, host="https://cloud.example") adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite", runner=runner) @@ -262,6 +434,321 @@ def authorization_binding(self, _workflow): return adapter, dispatch +def _terminal_delivery_chain( + dispatch: HostedDispatch, + *, + admission_sha256: str, + evidence_identity_sha256: str, + environment_digest: str, + registry_sha256: str, +) -> ProductionDeliveryPermitChain: + authority_key = Ed25519PrivateKey.from_private_bytes(bytes(range(32, 64))) + permit_payload = ProductionDeliveryPermitPayload( + execution_authority_id="00000000-0000-4000-8000-000000000008", + execution_authority_sha256="1" * 64, + permit_id="permit:hosted:1", + run_id=dispatch.run_id, + flow_run_id_sha256=hashlib.sha256(dispatch.run_id.encode("utf-8")).hexdigest(), + run_request_sha256="3" * 64, + action_request_sha256="4" * 64, + admission_artifact_sha256=admission_sha256, + evidence_identity_sha256=evidence_identity_sha256, + environment_digest=environment_digest, + qualification_signer_registry_sha256=registry_sha256, + qualification_signer_registry_revision=7, + qualification_signer_registry_checked_at="2026-08-26T11:59:30Z", + qualification_signer_registry_expires_at="2026-08-28T12:00:00Z", + input_edge_sequence=1, + authority_sequence=0, + issued_at="2026-08-26T12:00:00Z", + ) + permit = sign_production_delivery_permit(permit_payload, authority_key) + receipt_payload = ProductionDeliveryReceiptPayload( + execution_authority_id=permit_payload.execution_authority_id, + permit_id=permit_payload.permit_id, + permit_artifact_sha256=permit.artifact_sha256(), + authenticated_runner_id_sha256=hashlib.sha256( + dispatch.runner_id.encode("utf-8") + ).hexdigest(), + authenticated_session_id_sha256=hashlib.sha256( + dispatch.runner_session_id.encode("utf-8") + ).hexdigest(), + one_use_claim_id="00000000-0000-4000-8000-000000000010", + runtime_delivery_sequence=9, + delivered_at="2026-08-26T12:00:01Z", + ) + receipt = sign_production_delivery_receipt(receipt_payload, authority_key) + return ProductionDeliveryPermitChain.build( + (ProductionDeliveryPermit.build(permit, receipt),) + ) + + +def test_outer_adapter_builds_stores_rereads_and_verifies_terminal_v2( + monkeypatch, tmp_path, sealed +) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + verified_params = dict(dispatch.payload.params.values) + report = _production_report( + run_id_sha256=hashlib.sha256(dispatch.run_id.encode("utf-8")).hexdigest(), + bundle_content_digest=dispatch.payload.bundle.content_digest, + params=verified_params, + ) + authorization = dispatch.payload.authorization.model_copy( + update={ + "admitted_policy_name": report.governed_policy_name + or dispatch.payload.authorization.admitted_policy_name, + "admitted_policy_contract_sha256": (report.governed_policy_contract_sha256), + "execution_profile": report.execution_profile, + "minimum_effect_tier": report.governed_minimum_effect_tier, + "qualified_effect_requirements": tuple( + report.governed_qualified_effect_requirements + ), + "required_identity_step_ids": tuple(report.required_identity_step_ids), + "approval_source": report.governed_approval_source, + } + ) + payload = dispatch.payload.model_copy( + update={ + "authorization": authorization, + "dispatch_binding_sha256": dispatch_binding_sha256( + dispatch.run_id, authorization + ), + } + ) + dispatch = dispatch.model_copy(update={"payload": payload}) + report = report.model_copy( + update={ + "governed_authorization_id": authorization.authorization_id, + "governed_authorization_created_at": authorization.created_at, + "governed_approval_source": authorization.approval_source, + "governed_policy_name": authorization.admitted_policy_name, + "governed_runtime_inputs_digest": authorization.runtime_inputs_digest, + } + ) + private_key = _private_key() + public_key = private_key.public_key().public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw, + ) + admission_sha256 = "5" * 64 + evidence_identity_sha256 = "6" * 64 + environment_digest = "7" * 64 + registry_sha256 = "8" * 64 + qualification_sha256 = "f" * 64 + evidence_identity = SimpleNamespace( + admission_policy_sha256="9" * 64, + artifact_sha256=lambda: evidence_identity_sha256, + ) + runtime = SimpleNamespace( + substrate="web", + artifact_sha256=lambda: "0" * 64, + ) + expected = SimpleNamespace( + bundle_artifact_sha256="b" * 64, + bundle_content_digest=dispatch.payload.bundle.content_digest, + environment_digest=environment_digest, + environment_contract_sha256="a" * 64, + runtime_environment_sha256="b" * 64, + identity_contract_sha256="c" * 64, + effect_contract_sha256="d" * 64, + runtime_validation_id="00000000-0000-4000-8000-000000000006", + runtime_build_identity=runtime, + evidence_runner_signer_sha256=evidence_runner_signer_sha256(public_key), + ) + admission = SimpleNamespace( + payload=SimpleNamespace( + admission_id="00000000-0000-4000-8000-000000000001", + evidence_identity=evidence_identity, + ) + ) + qualification = SimpleNamespace( + qualification_admission_sha256=admission_sha256, + qualification_admission=admission, + expected=expected, + qualification_signer_registry_sha256=registry_sha256, + qualification_signer_registry=SimpleNamespace( + revision=7, + expires_at="2026-08-28T12:00:00Z", + ), + immutable_binding_sha256=lambda: qualification_sha256, + ) + chain = _terminal_delivery_chain( + dispatch, + admission_sha256=admission_sha256, + evidence_identity_sha256=evidence_identity_sha256, + environment_digest=environment_digest, + registry_sha256=registry_sha256, + ) + binding = dispatch.payload.dispatch_binding_sha256 + local_authorization = authorization.model_copy( + update={ + "production_qualification_admission_id": (admission.payload.admission_id), + "production_qualification_admission_sha256": admission_sha256, + "production_qualification_evidence_identity_sha256": ( + evidence_identity_sha256 + ), + "production_qualification_runtime_validation_id": ( + expected.runtime_validation_id + ), + "production_qualification_signer_registry_sha256": registry_sha256, + "production_qualification_signer_registry_revision": 7, + "production_qualification_signer_registry_expires_at": ( + "2026-08-28T12:00:00Z" + ), + "production_qualification_authority_sha256": qualification_sha256, + } + ) + manifest = SimpleNamespace( + delivery_authority_kind="cloud_runner", + remote_delivery_run_id=dispatch.run_id, + managed_dispatch_binding_sha256=binding, + params=verified_params, + governed_authorization=local_authorization, + ) + + class Store: + def __init__(self, _run_dir): + pass + + def read_manifest(self): + return manifest + + class Authority: + def __init__(self, _run_dir, _store): + pass + + def production_delivery_permit_chain(self): + return chain + + fixed_now = datetime(2026, 8, 26, 12, 0, 2, tzinfo=timezone.utc) + + class FixedDatetime(datetime): + @classmethod + def now(cls, tz=None): + return fixed_now if tz is not None else fixed_now.replace(tzinfo=None) + + monkeypatch.setattr(hosted, "CheckpointStore", Store) + monkeypatch.setattr(hosted, "DurableAuthority", Authority) + monkeypatch.setattr(hosted, "datetime", FixedDatetime) + run_dir = tmp_path / "run" + run_dir.mkdir(mode=0o700) + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite") + + mismatched_dir = tmp_path / "mismatched-run" + mismatched_dir.mkdir(mode=0o700) + with pytest.raises(ValueError, match="run report differs"): + adapter._produce_terminal_verification( + dispatch=dispatch, + report=report.model_copy(update={"params": {"visit_date": "wrong"}}), + run_dir=mismatched_dir, + qualification=qualification, + private_key=private_key, + verified_params=verified_params, + dispatch_binding_sha256=binding, + ) + assert not (mismatched_dir / "production-terminal-report.json").exists() + assert not (mismatched_dir / "production-terminal-verification.json").exists() + + storage_failure_dir = tmp_path / "storage-failure-run" + storage_failure_dir.mkdir(mode=0o700) + original_read = adapter._read_private_bytes + + def fail_proof_reread(path, *, maximum_bytes, label): + if label == "production terminal verification": + raise OSError("simulated protected storage failure") + return original_read(path, maximum_bytes=maximum_bytes, label=label) + + monkeypatch.setattr(adapter, "_read_private_bytes", fail_proof_reread) + with pytest.raises(OSError, match="storage failure"): + adapter._produce_terminal_verification( + dispatch=dispatch, + report=report, + run_dir=storage_failure_dir, + qualification=qualification, + private_key=private_key, + verified_params=verified_params, + dispatch_binding_sha256=binding, + ) + assert not (storage_failure_dir / "production-terminal-report.json").exists() + assert not (storage_failure_dir / "production-terminal-verification.json").exists() + monkeypatch.setattr(adapter, "_read_private_bytes", original_read) + + proof, report_sha256 = adapter._produce_terminal_verification( + dispatch=dispatch, + report=report, + run_dir=run_dir, + qualification=qualification, + private_key=private_key, + verified_params=verified_params, + dispatch_binding_sha256=binding, + ) + + report_path = run_dir / "production-terminal-report.json" + proof_path = run_dir / "production-terminal-verification.json" + assert hashlib.sha256(report_path.read_bytes()).hexdigest() == report_sha256 + assert proof_path.read_bytes() == hosted.canonical_json(proof) + assert hashlib.sha256(proof_path.read_bytes()).hexdigest() == ( + proof.artifact_sha256() + ) + if os.name != "nt": + assert report_path.stat().st_mode & 0o777 == 0o600 + assert proof_path.stat().st_mode & 0o777 == 0o600 + assert "2026-07-01" in report_path.read_text(encoding="utf-8") + assert "2026-07-01" not in proof_path.read_text(encoding="utf-8") + + +def test_terminal_admission_is_revalidated_after_child_execution( + monkeypatch, tmp_path, config, sealed +) -> None: + workflow, _ = sealed + report = _production_report() + calls = 0 + + def runner(_argv, _run_dir, _child_env): + nonlocal calls + calls += 1 + return ManagedExecution( + returncode=0, + report_bytes=report.model_dump_json().encode(), + ) + + adapter, dispatch = _prepared_adapter( + monkeypatch, tmp_path, config, workflow, runner + ) + release_checks = 0 + + def verify_release(*_args): + nonlocal release_checks + release_checks += 1 + if release_checks == 2: + raise ValueError("release admission was revoked during execution") + + monkeypatch.setattr(adapter, "_verify_product_release", verify_release) + monkeypatch.setattr( + adapter, + "_produce_terminal_verification", + lambda **_kwargs: pytest.fail("revoked run must not produce terminal proof"), + ) + authority = DeliveryAuthority( + dispatch.managed_delivery_authority_url, + dispatch.delivery_authority_token, + ) + + first = adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run", + authority=authority, + ) + + assert first.outcome is TransactionOutcome.RECONCILIATION_REQUIRED + assert first.started is True + assert first.terminal_verification is None + assert calls == 1 + assert release_checks == 2 + + @pytest.mark.parametrize( ("fault", "execution"), [ @@ -684,3 +1171,61 @@ def test_parsed_refusal_callback_contains_closed_terminal(tmp_path, sealed) -> N assert terminal["outcome"] == "REJECTED_POLICY" assert terminal["started"] is False assert terminal["uncertain_delivery"] is False + + +def test_recovery_callback_retains_exact_terminal_v2_envelope(tmp_path, sealed) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite") + proof = sign_production_terminal_verification(_payload(), _private_key()) + binding = adapter.recovery_binding(dispatch).model_copy( + update={"run_id": proof.payload.run_id} + ) + result = HostedRunResult( + dispatch_id=dispatch.dispatch_id, + run_id=binding.run_id, + outcome=TransactionOutcome.VERIFIED, + evidence_batch=(), + terminal_verification=proof, + started=True, + uncertain_delivery=False, + report_sha256=proof.payload.run_report_sha256, + ) + + callback = adapter.callback_request(binding, result) + terminal = HostedTerminalEvent.model_validate(callback.events[-1]) + assert terminal.terminal_verification_artifact_bytes_base64 is not None + raw = b64decode(terminal.terminal_verification_artifact_bytes_base64, validate=True) + assert hashlib.sha256(raw).hexdigest() == ( + terminal.terminal_verification_artifact_sha256 + ) + decoded = json.loads(raw) + assert decoded["payload"]["schema_version"] == ( + "openadapt.production-terminal-verification/v2" + ) + assert "params" not in decoded["payload"] + assert "report" not in decoded["payload"] + assert callback.runner_session_id == dispatch.runner_session_id + assert callback.workflow_admission_sha256 == ( + dispatch.workflow_admission.artifact_sha256 + ) + + +def test_callback_refuses_terminal_proof_for_a_different_run(tmp_path, sealed) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite") + proof = sign_production_terminal_verification(_payload(), _private_key()) + result = HostedRunResult.model_construct( + dispatch_id=dispatch.dispatch_id, + run_id=dispatch.run_id, + outcome=TransactionOutcome.VERIFIED, + evidence_batch=(), + terminal_verification=proof, + started=True, + uncertain_delivery=False, + report_sha256=proof.payload.run_report_sha256, + ) + + with pytest.raises(ValueError, match="different run report"): + adapter.callback_request(adapter.recovery_binding(dispatch), result) diff --git a/tests/test_program_ir_phase1.py b/tests/test_program_ir_phase1.py index 63d43377..7f19ac29 100644 --- a/tests/test_program_ir_phase1.py +++ b/tests/test_program_ir_phase1.py @@ -182,6 +182,29 @@ def test_missing_required_param_fails_fast_naming_it(bundle, run_dir): assert backend.actions == [] # nothing ran +@pytest.mark.parametrize( + ("kind", "value"), + [(ParamKind.BOOLEAN, False), (ParamKind.NUMBER, 0)], +) +def test_required_false_and_zero_are_present(kind, value, bundle, run_dir): + wf = Workflow( + name="wf", + param_specs={"required": ParamSpec(name="required", type=kind, required=True)}, + steps=[key_step()], + ) + backend = FakeBackend() + report = Replayer(backend, vision=FakeVision()).run( + wf, + params={"required": value}, + bundle_dir=bundle, + run_dir=run_dir, + ) + assert report.success is True + assert report.params["required"] == value + assert type(report.params["required"]) is type(value) + assert backend.actions == [("press", "Enter")] + + # -- wait_until: bounded readiness, fail-safe HALT on timeout ----------------- diff --git a/tests/test_runner_client_lib.py b/tests/test_runner_client_lib.py index 89e1786e..482c3d48 100644 --- a/tests/test_runner_client_lib.py +++ b/tests/test_runner_client_lib.py @@ -492,6 +492,44 @@ def test_full_admit_returns_execution_snapshot(self, sealed, config): assert verdict.effect_covered_consequential_steps == 0 assert verdict.workflow.manifest is not None + def test_param_domains_use_canonical_scalar_text(self, tmp_path, sealed, profile): + workflow, bundle = sealed + params = { + "enabled": False, + "count": 0, + "small": 1e-7, + "fixed": 1e20, + "large": 1e21, + } + manifest = write_manifest( + tmp_path, + f""" +[runner] +name = "n" +[profiles] +default = "{profile}" +[[bundles]] +content_digest = "{workflow.manifest.content_digest}" +path = "{bundle}" +[bundles.param_patterns] +enabled = '^false$' +count = '^0$' +small = '^1e-7$' +fixed = '^100000000000000000000$' +large = '^1e[+]21$' +""", + ) + cfg = load_runner_config(manifest) + authorization = mint_authorization(workflow, params) + verdict = verified_or_refusal( + workflow, + cfg, + params={"values": params}, + authorization=authorization, + ) + assert not isinstance(verdict, Refusal) + assert verdict.params == params + class TestVerifyRefusals: def test_unknown_job_kind(self, sealed, config): diff --git a/uv.lock b/uv.lock index 0bacf324..efcc28a9 100644 --- a/uv.lock +++ b/uv.lock @@ -2136,7 +2136,7 @@ wheels = [ [[package]] name = "openadapt-flow" -version = "1.33.0" +version = "1.34.0" source = { editable = "." } dependencies = [ { name = "cryptography" }, From 9fb807bd6188691dde0da1f967a58a08ec1d16a1 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Wed, 26 Aug 2026 17:56:59 -0400 Subject: [PATCH 4/9] test: preserve boolean parameter digest --- tests/test_run_gate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_run_gate.py b/tests/test_run_gate.py index 09f53c76..83be1a2e 100644 --- a/tests/test_run_gate.py +++ b/tests/test_run_gate.py @@ -1389,7 +1389,7 @@ def capture(args): expected = { "patient_id": secret_value, "count": "3", - "approved": "True", + "approved": True, } authorization = captured["authorization"] assert authorization.runtime_inputs_digest == runtime_inputs_digest( From a3343adddce760a6839bc49f8af687ba4c97bc3e Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Thu, 27 Aug 2026 13:31:29 -0400 Subject: [PATCH 5/9] feat(hosted): sign complete terminal outcomes --- openadapt_flow/runner/hosted_adapter.py | 117 ++- openadapt_flow/runtime/durable/authority.py | 6 +- openadapt_flow/terminal_verification_v2.py | 805 ++++++++++++++---- ...inal_verification_v2_terminal_vectors.json | 1 + tests/test_durable_authority_v13.py | 19 + tests/test_hosted_runner_adapter.py | 80 +- tests/test_terminal_verification_v2.py | 660 +++++++++++++- 7 files changed, 1487 insertions(+), 201 deletions(-) create mode 100644 tests/fixtures/terminal_verification_v2_terminal_vectors.json diff --git a/openadapt_flow/runner/hosted_adapter.py b/openadapt_flow/runner/hosted_adapter.py index 9ee003fb..7fd50a0c 100644 --- a/openadapt_flow/runner/hosted_adapter.py +++ b/openadapt_flow/runner/hosted_adapter.py @@ -235,6 +235,9 @@ class HostedDispatch(_Closed): run_id: str = Field(pattern=_UUID) workflow_id: str = Field(pattern=_UUID) workflow_version_id: str = Field(pattern=_UUID) + execution_authority_id: str = Field(pattern=_UUID) + execution_authority_sha256: str = Field(pattern=_HEX64) + execution_authority_signer_sha256: str = Field(pattern=_HEX64) idempotency_key: str = Field(pattern=_IDEMPOTENCY) lease_token: str = Field(pattern=_LEASE_TOKEN, repr=False) lease_expires_at: str @@ -306,14 +309,22 @@ class HostedTerminalEvent(_Closed): ) @model_validator(mode="after") - def _verified_requires_exact_proof(self) -> "HostedTerminalEvent": + def _terminal_outcome_requires_exact_proof(self) -> "HostedTerminalEvent": has_proof = self.terminal_verification_artifact_bytes_base64 is not None if has_proof != (self.terminal_verification_artifact_sha256 is not None): raise ValueError("terminal verification binding is incomplete") - if self.outcome == "VERIFIED" and not has_proof: - raise ValueError("VERIFIED requires exact terminal verification") - if self.outcome != "VERIFIED" and has_proof: - raise ValueError("non-VERIFIED callback cannot carry a success proof") + if self.outcome in {"VERIFIED", "HALTED_BEFORE_EFFECT"} and not has_proof: + raise ValueError(f"{self.outcome} requires exact terminal verification") + if ( + self.outcome + not in { + "VERIFIED", + "HALTED_BEFORE_EFFECT", + "RECONCILIATION_REQUIRED", + } + and has_proof + ): + raise ValueError("terminal callback outcome cannot carry a v2 proof") if has_proof: assert self.terminal_verification_artifact_bytes_base64 is not None assert self.terminal_verification_artifact_sha256 is not None @@ -339,6 +350,8 @@ def _verified_requires_exact_proof(self) -> "HostedTerminalEvent": or proof.payload.run_report_object_sha256 != self.report_sha256 ): raise ValueError("terminal verification names a different run report") + if proof.payload.run_receipt.transaction_outcome != self.outcome: + raise ValueError("terminal verification names a different outcome") return self @@ -355,14 +368,24 @@ class HostedRunResult(_Closed): @model_validator(mode="after") def _closed_terminal(self) -> "HostedRunResult": - if (self.outcome is TransactionOutcome.VERIFIED) != ( - self.terminal_verification is not None + if ( + self.outcome + in { + TransactionOutcome.VERIFIED, + TransactionOutcome.HALTED_BEFORE_EFFECT, + } + and self.terminal_verification is None ): - raise ValueError("only a terminally verified result can be VERIFIED") - if self.uncertain_delivery and self.outcome not in { - TransactionOutcome.RECONCILIATION_REQUIRED, + raise ValueError("closed terminal outcome requires a signed v2 proof") + if self.terminal_verification is not None and self.outcome not in { TransactionOutcome.VERIFIED, + TransactionOutcome.HALTED_BEFORE_EFFECT, + TransactionOutcome.RECONCILIATION_REQUIRED, }: + raise ValueError("terminal outcome cannot carry a signed v2 proof") + if self.uncertain_delivery != ( + self.outcome is TransactionOutcome.RECONCILIATION_REQUIRED + ): raise ValueError("uncertain delivery has an invalid terminal outcome") if self.terminal_verification is not None and ( self.terminal_verification.payload.run_id != self.run_id @@ -370,8 +393,12 @@ def _closed_terminal(self) -> "HostedRunResult": != self.report_sha256 or self.terminal_verification.payload.run_report_object_sha256 != self.report_sha256 + or self.terminal_verification.payload.run_receipt.transaction_outcome + != self.outcome.value ): - raise ValueError("terminal verification names a different run report") + raise ValueError( + "terminal verification names a different run report or outcome" + ) return self @@ -1171,8 +1198,13 @@ def _produce_terminal_verification( ): raise ValueError("run report differs from retained governed inputs") - chain = DurableAuthority(run_dir, store).production_delivery_permit_chain() - first = chain.entries[0] + prepared = prepare_production_terminal_evidence(report) + chain = DurableAuthority(run_dir, store).production_delivery_permit_chain( + allow_empty=( + prepared.transaction_outcome is TransactionOutcome.HALTED_BEFORE_EFFECT + ) + ) + first = chain.entries[0] if chain.entries else None expected = qualification.expected runtime = expected.runtime_build_identity flow_run_id_sha256 = hashlib.sha256(dispatch.run_id.encode("utf-8")).hexdigest() @@ -1182,9 +1214,13 @@ def _produce_terminal_verification( runner_session_id_sha256 = hashlib.sha256( dispatch.runner_session_id.encode("utf-8") ).hexdigest() - if ( + if first is not None and ( first.run_id != dispatch.run_id or first.flow_run_id_sha256 != flow_run_id_sha256 + or first.execution_authority_id != dispatch.execution_authority_id + or first.execution_authority_sha256 != dispatch.execution_authority_sha256 + or first.authority_signer_sha256 + != dispatch.execution_authority_signer_sha256 or first.admission_artifact_sha256 != qualification.qualification_admission_sha256 or first.evidence_identity_sha256 @@ -1199,7 +1235,6 @@ def _produce_terminal_verification( ): raise ValueError("retained delivery chain differs from admitted live state") - prepared = prepare_production_terminal_evidence(report) now = datetime.now(timezone.utc).replace(microsecond=0) now_text = now.isoformat().replace("+00:00", "Z") context = ProductionTerminalVerificationContext( @@ -1228,9 +1263,11 @@ def _produce_terminal_verification( qualification_signer_registry_revision=( qualification.qualification_signer_registry.revision ), - execution_authority_id=first.execution_authority_id, - execution_authority_sha256=first.execution_authority_sha256, - execution_authority_signer_sha256=first.authority_signer_sha256, + execution_authority_id=dispatch.execution_authority_id, + execution_authority_sha256=dispatch.execution_authority_sha256, + execution_authority_signer_sha256=( + dispatch.execution_authority_signer_sha256 + ), permit_chain=chain, run_report_object_version="sha256:" + prepared.report_sha256, verified_at=now_text, @@ -1248,7 +1285,7 @@ def _produce_terminal_verification( raise ValueError("terminal report changed during proof production") envelope_bytes = canonical_json(built.envelope) payload = built.envelope.payload - final = chain.entries[-1] + final = chain.entries[-1] if chain.entries else None live_expected = ProductionTerminalVerificationExpected( run_id=dispatch.run_id, flow_run_id_sha256=flow_run_id_sha256, @@ -1277,13 +1314,19 @@ def _produce_terminal_verification( qualification_signer_registry_revision=( qualification.qualification_signer_registry.revision ), - execution_authority_id=first.execution_authority_id, - execution_authority_sha256=first.execution_authority_sha256, - execution_authority_signer_sha256=first.authority_signer_sha256, + execution_authority_id=dispatch.execution_authority_id, + execution_authority_sha256=dispatch.execution_authority_sha256, + execution_authority_signer_sha256=( + dispatch.execution_authority_signer_sha256 + ), permit_chain_sha256=chain.permit_chain_sha256, permit_count=len(chain.entries), - final_authority_sequence=final.authority_sequence, - final_runtime_delivery_sequence=final.runtime_delivery_sequence, + final_authority_sequence=( + final.authority_sequence if final is not None else 0 + ), + final_runtime_delivery_sequence=( + final.runtime_delivery_sequence if final is not None else 0 + ), authenticated_runner_id_sha256=runner_id_sha256, authenticated_session_id_sha256=runner_session_id_sha256, acknowledged_one_use_claim_ids=tuple( @@ -1624,8 +1667,12 @@ def execute( proof: ProductionTerminalVerificationEnvelope | None = None if execution.terminal_verification is not None: raise ValueError("managed child supplied an untrusted terminal proof") - if outcome is TransactionOutcome.VERIFIED: - if execution.returncode != 0: + if outcome in { + TransactionOutcome.VERIFIED, + TransactionOutcome.HALTED_BEFORE_EFFECT, + TransactionOutcome.RECONCILIATION_REQUIRED, + }: + if outcome is TransactionOutcome.VERIFIED and execution.returncode != 0: raise ValueError("managed child exited unsuccessfully") terminal_config = load_runner_config(runner_config, protected=True) if self._protected_runner_origin(terminal_config) != configured_origin: @@ -1653,11 +1700,20 @@ def execute( verified_params=params, dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, ) + if proof.payload.run_receipt.transaction_outcome != outcome.value: + raise ValueError("terminal proof outcome differs from the report") except Exception: # noqa: BLE001 - post-delivery terminalization fails closed outcome = TransactionOutcome.RECONCILIATION_REQUIRED proof = None - if outcome is not TransactionOutcome.VERIFIED: - proof = None + if ( + outcome + in { + TransactionOutcome.VERIFIED, + TransactionOutcome.HALTED_BEFORE_EFFECT, + } + and proof is None + ): + outcome = TransactionOutcome.RECONCILIATION_REQUIRED self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) if report is None: events = tuple( @@ -1681,10 +1737,7 @@ def execute( ), ) ) - uncertain = outcome is TransactionOutcome.RECONCILIATION_REQUIRED or any( - result.delivery_uncertainty is not None - for result in (report.results if report is not None else ()) - ) + uncertain = outcome is TransactionOutcome.RECONCILIATION_REQUIRED return HostedRunResult( dispatch_id=parsed.dispatch_id, run_id=parsed.run_id, diff --git a/openadapt_flow/runtime/durable/authority.py b/openadapt_flow/runtime/durable/authority.py index dde650d8..08113081 100644 --- a/openadapt_flow/runtime/durable/authority.py +++ b/openadapt_flow/runtime/durable/authority.py @@ -2418,7 +2418,9 @@ def acknowledge_remote_delivery( self._emit_synthetic_delivery_marker(marker_payload) return entry - def production_delivery_permit_chain(self) -> ProductionDeliveryPermitChain: + def production_delivery_permit_chain( + self, *, allow_empty: bool = False + ) -> ProductionDeliveryPermitChain: """Rebuild the exact acknowledged chain from protected retained bytes.""" with self._transaction() as connection: @@ -2433,6 +2435,8 @@ def production_delivery_permit_chain(self) -> ProductionDeliveryPermitChain: (self.path_key,), ).fetchall() if not rows: + if allow_empty: + return ProductionDeliveryPermitChain.build(()) raise DurableAuthorityBusy( "the production delivery permit chain is unavailable" ) diff --git a/openadapt_flow/terminal_verification_v2.py b/openadapt_flow/terminal_verification_v2.py index 925df798..93a13750 100644 --- a/openadapt_flow/terminal_verification_v2.py +++ b/openadapt_flow/terminal_verification_v2.py @@ -1,9 +1,9 @@ """Production terminal-verification v2 contracts and producer. -Flow builds one success-only artifact after it revalidates a complete VERIFIED -report, run receipt, signed permit chain, and class-separated evidence. The -acceptor must reconstruct every value from immutable storage before it admits -the result as Production success. +Flow builds one signed terminal artifact after it revalidates a complete +VERIFIED, HALTED_BEFORE_EFFECT, or RECONCILIATION_REQUIRED report. The +acceptor reconstructs every value from immutable storage. Only a VERIFIED +artifact can authorize a Production success or a billable result. """ from __future__ import annotations @@ -32,13 +32,31 @@ model_validator, ) -from openadapt_flow.ir import PostconditionContractEvidence, RunReport +from openadapt_flow.ir import ( + EffectVerificationEvidence, + PostconditionContractEvidence, + RunReport, +) from openadapt_flow.qualification_admission_v2 import ( MAX_PERMIT_SNAPSHOT_AGE, ClosedSignedModel, canonical_json, ) -from openadapt_flow.receipt import ReceiptError, RunReceipt, build_receipt +from openadapt_flow.receipt import ( + ReceiptError, + RunReceipt, + _hour_utc, + _over_halt_count, + _receipt_builder_version, + build_receipt, +) +from openadapt_flow.transaction import ( + TransactionOutcome, + _attempt_state, + _effect_absence_proven, + _is_consequential_result, + classify_transaction_outcome, +) SCHEMA: Final[Literal["openadapt.production-terminal-verification/v2"]] = ( "openadapt.production-terminal-verification/v2" @@ -104,6 +122,7 @@ class PreparedProductionTerminalEvidence: execution_outcome: "ProductionExecutionOutcome" run_receipt: "ProductionRunReceipt" run_receipt_sha256: str + transaction_outcome: TransactionOutcome report: RunReport = dataclass_field(repr=False) @@ -130,10 +149,10 @@ def evidence_runner_signer_sha256(public_key: bytes) -> str: class TerminalContractCounts(ClosedSignedModel): - authorization: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - identity: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - postcondition: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - effect: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) + authorization: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + identity: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + postcondition: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + effect: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) class ProductionExecutionOutcome(ClosedSignedModel): @@ -142,11 +161,16 @@ class ProductionExecutionOutcome(ClosedSignedModel): version: Literal["openadapt.execution-outcome/v1"] = ( "openadapt.execution-outcome/v1" ) - outcome: Literal["VERIFIED"] = "VERIFIED" + outcome: Literal[ + "VERIFIED", + "HALTED", + "FAILED", + "ROLLED_BACK", + ] = "VERIFIED" profile: Literal["standard", "regulated"] - production_eligible: Literal[True] = True + production_eligible: StrictBool = True qualification_evidence_only: Literal[False] = False - execution_completed: Literal[True] = True + execution_completed: StrictBool = True required_contracts: TerminalContractCounts passed_contracts: TerminalContractCounts workflow_contract_sha256: str = Field(pattern=_SHA256_RE) @@ -164,27 +188,33 @@ class ProductionExecutionOutcome(ClosedSignedModel): "model", ], ..., - ] = Field(min_length=4, max_length=7) + ] = Field(min_length=1, max_length=7) model_calls: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) external_network_calls: Literal["none", "observed"] - compensation_actions: Literal[0] = 0 + compensation_actions: StrictInt = Field(default=0, ge=0, le=JS_MAX_SAFE_INTEGER) @model_validator(mode="after") - def _complete_verified_outcome(self) -> "ProductionExecutionOutcome": - if self.passed_contracts != self.required_contracts: - raise ValueError( - "terminal VERIFIED outcome lacks complete contract coverage" - ) + def _complete_terminal_outcome(self) -> "ProductionExecutionOutcome": + required = self.required_contracts.model_dump(mode="python") + passed = self.passed_contracts.model_dump(mode="python") + if any(passed[key] > required[key] for key in required): + raise ValueError("terminal passed contract counts exceed required counts") if self.required_contracts.authorization != 1: - raise ValueError("terminal VERIFIED outcome requires one authorization") + raise ValueError("terminal outcome requires one authorization") + if self.passed_contracts.authorization != 1: + raise ValueError("terminal outcome requires one passed authorization") if len(self.postcondition_evidence) != self.required_contracts.postcondition: raise ValueError("terminal postcondition evidence cardinality is invalid") + if ( + sum(item.verdict == "passed" for item in self.postcondition_evidence) + != self.passed_contracts.postcondition + ): + raise ValueError("terminal postcondition evidence count is invalid") if any( - item.verdict != "passed" - or item.workflow_contract_sha256 != self.workflow_contract_sha256 + item.workflow_contract_sha256 != self.workflow_contract_sha256 for item in self.postcondition_evidence ): - raise ValueError("terminal postcondition evidence is not fully verified") + raise ValueError("terminal postcondition evidence binding is invalid") keys = tuple( (item.result_index, item.contract_kind, item.contract_index) for item in self.postcondition_evidence @@ -202,20 +232,47 @@ def _complete_verified_outcome(self) -> "ProductionExecutionOutcome": self.evidence_classes ) != len(set(self.evidence_classes)): raise ValueError("terminal evidence classes must be unique and ordered") - required_classes = {"authorization", "identity", "postcondition"} - if not required_classes.issubset(self.evidence_classes): - raise ValueError( - "terminal VERIFIED outcome lacks required evidence classes" - ) + if "authorization" not in self.evidence_classes: + raise ValueError("terminal outcome lacks authorization evidence") + if (self.passed_contracts.identity > 0) != ( + "identity" in self.evidence_classes + ): + raise ValueError("terminal identity evidence class is invalid") + if (self.passed_contracts.postcondition > 0) != ( + "postcondition" in self.evidence_classes + ): + raise ValueError("terminal postcondition evidence class is invalid") effect_classes = { item for item in self.evidence_classes if item.startswith("effect_tier_") } - if len(effect_classes) != 1: - raise ValueError("terminal VERIFIED outcome requires one effect tier") + if (self.passed_contracts.effect > 0) != bool(effect_classes) or len( + effect_classes + ) > 1: + raise ValueError("terminal effect evidence classes are invalid") if (self.model_calls > 0) != ("model" in self.evidence_classes): raise ValueError("terminal model evidence does not match its call count") if self.model_calls > 0 and self.external_network_calls != "observed": raise ValueError("terminal model calls require observed network evidence") + if (self.compensation_actions > 0) != (self.outcome == "ROLLED_BACK"): + raise ValueError("terminal compensation evidence is invalid") + if self.outcome == "VERIFIED": + if self.passed_contracts != self.required_contracts: + raise ValueError( + "terminal VERIFIED outcome lacks complete contract coverage" + ) + if not self.production_eligible or not self.execution_completed: + raise ValueError("terminal VERIFIED outcome is not production complete") + required_classes = {"authorization", "identity", "postcondition"} + if not required_classes.issubset(self.evidence_classes): + raise ValueError( + "terminal VERIFIED outcome lacks required evidence classes" + ) + if len(effect_classes) != 1: + raise ValueError("terminal VERIFIED outcome requires one effect tier") + elif self.production_eligible: + raise ValueError("only a VERIFIED terminal outcome is production eligible") + elif self.outcome == "HALTED" and self.execution_completed: + raise ValueError("terminal HALTED outcome cannot claim completed execution") return self def artifact_sha256(self) -> str: @@ -225,20 +282,25 @@ def artifact_sha256(self) -> str: class ProductionRunReceipt(ClosedSignedModel): - """Cross-language integer projection of every current RunReceipt field.""" + """Cross-language integer projection of one terminal report.""" schema_version: Literal["openadapt.production-run-receipt/v1"] = ( "openadapt.production-run-receipt/v1" ) - source_schema_version: Literal["openadapt.run-receipt/v2"] = ( - "openadapt.run-receipt/v2" - ) - outcome: Literal["VERIFIED"] - transaction_outcome: Literal["VERIFIED"] + source_schema_version: Literal[ + "openadapt.run-receipt/v2", + "openadapt.run-report/v1", + ] + outcome: Literal["VERIFIED", "HALTED", "FAILED", "ROLLED_BACK"] + transaction_outcome: Literal[ + "VERIFIED", + "HALTED_BEFORE_EFFECT", + "RECONCILIATION_REQUIRED", + ] profile: Literal["standard", "regulated"] - production_eligible: Literal[True] + production_eligible: StrictBool steps_total: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - steps_ok: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) + steps_ok: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) heals: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) model_calls: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) est_cost_microusd: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) @@ -266,8 +328,9 @@ class ProductionRunReceipt(ClosedSignedModel): "model", ], ..., - ] = Field(min_length=4, max_length=7) + ] = Field(min_length=1, max_length=7) effect_tier_reached: Literal[ + "none", "independent_system", "independent_session", "persisted_state_reacquisition", @@ -275,14 +338,14 @@ class ProductionRunReceipt(ClosedSignedModel): authorization_required: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) authorization_confirmed: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) identity_required: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - identity_confirmed: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) + identity_confirmed: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) postconditions_required: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - postconditions_confirmed: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - effects_required: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - effects_confirmed: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - identity_armed: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - identity_applicable: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - over_halt_count: Literal[0] + postconditions_confirmed: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + effects_required: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + effects_confirmed: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + identity_armed: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + identity_applicable: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + over_halt_count: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) substrate: Literal["web", "windows", "macos", "linux", "rdp", "citrix"] provenance: Literal["production"] receipt_builder_version: str = Field( @@ -310,14 +373,14 @@ def _closed_receipt(self) -> "ProductionRunReceipt": ), ("effect", self.effects_required, self.effects_confirmed), ): - if required != confirmed: - raise ValueError(f"production receipt lacks complete {label} coverage") + if confirmed > required: + raise ValueError(f"terminal receipt exceeds required {label} coverage") if self.authorization_required != 1: - raise ValueError("production receipt requires one authorization") - if self.steps_ok != self.steps_total: - raise ValueError("production receipt requires all steps to succeed") - if self.identity_armed != self.identity_applicable: - raise ValueError("production receipt requires complete identity arming") + raise ValueError("terminal receipt requires one authorization") + if self.authorization_confirmed != 1: + raise ValueError("terminal receipt requires one passed authorization") + if self.steps_ok > self.steps_total: + raise ValueError("terminal receipt successful step count is invalid") if self.evidence_classes != tuple(sorted(self.evidence_classes)) or len( self.evidence_classes ) != len(set(self.evidence_classes)): @@ -327,6 +390,67 @@ def _closed_receipt(self) -> "ProductionRunReceipt": for value in self.rung_histogram.values() ): raise ValueError("production receipt rung counts are invalid") + effect_classes = { + item for item in self.evidence_classes if item.startswith("effect_tier_") + } + expected_effect_tier = { + "effect_tier_1": "independent_system", + "effect_tier_2": "independent_session", + "effect_tier_3": "persisted_state_reacquisition", + } + if self.effects_confirmed == 0: + if effect_classes or self.effect_tier_reached != "none": + raise ValueError("terminal receipt effect evidence is inconsistent") + elif ( + len(effect_classes) != 1 + or self.effect_tier_reached + != (expected_effect_tier[next(iter(effect_classes))]) + ): + raise ValueError("terminal receipt effect tier is inconsistent") + if self.transaction_outcome == "VERIFIED": + if ( + self.outcome != "VERIFIED" + or not self.production_eligible + or self.source_schema_version != "openadapt.run-receipt/v2" + or self.steps_ok != self.steps_total + or self.identity_armed != self.identity_applicable + or self.over_halt_count != 0 + or self.effect_tier_reached == "none" + ): + raise ValueError("VERIFIED terminal receipt is incomplete") + for label, required, confirmed in ( + ( + "authorization", + self.authorization_required, + self.authorization_confirmed, + ), + ("identity", self.identity_required, self.identity_confirmed), + ( + "postcondition", + self.postconditions_required, + self.postconditions_confirmed, + ), + ("effect", self.effects_required, self.effects_confirmed), + ): + if required != confirmed: + raise ValueError( + f"VERIFIED terminal receipt lacks complete {label} coverage" + ) + elif self.production_eligible: + raise ValueError( + "non-VERIFIED terminal receipt cannot be production eligible" + ) + elif self.source_schema_version != "openadapt.run-report/v1": + raise ValueError( + "non-VERIFIED terminal receipt must derive from its report" + ) + elif self.transaction_outcome == "HALTED_BEFORE_EFFECT": + if ( + self.outcome != "HALTED" + or self.effects_confirmed != 0 + or self.effect_tier_reached != "none" + ): + raise ValueError("HALTED terminal receipt does not prove no effect") return self @@ -347,6 +471,7 @@ def project_production_run_receipt(receipt: RunReceipt) -> ProductionRunReceipt: # instead of a static cast; a value outside the production subset raises. return ProductionRunReceipt.model_validate( { + "source_schema_version": "openadapt.run-receipt/v2", "outcome": receipt.outcome, "transaction_outcome": receipt.transaction_outcome, "profile": receipt.profile, @@ -383,10 +508,128 @@ def project_production_run_receipt(receipt: RunReceipt) -> ProductionRunReceipt: ) +def project_production_terminal_run_receipt( + report: RunReport, + *, + transaction_outcome: TransactionOutcome, +) -> ProductionRunReceipt: + """Project a non-success terminal report without creating a success receipt.""" + + if transaction_outcome not in { + TransactionOutcome.HALTED_BEFORE_EFFECT, + TransactionOutcome.RECONCILIATION_REQUIRED, + }: + raise ProductionTerminalVerificationError( + "non-success terminal receipt has an invalid transaction outcome" + ) + envelope = report.outcome_envelope + if ( + envelope is None + or report.execution_outcome not in {"HALTED", "FAILED", "ROLLED_BACK"} + or report.execution_profile not in {"standard", "regulated"} + or report.production_eligible + or report.execution_completed is True + and report.execution_outcome == "HALTED" + or report.transaction_outcome != transaction_outcome.value + or report.transaction_billable is not False + or report.bundle_content_digest is None + ): + raise ProductionTerminalVerificationError( + "terminal report lacks its closed non-success transaction contract" + ) + if not report.results: + raise ProductionTerminalVerificationError( + "terminal report contains no retained step result" + ) + report_bytes = canonical_json(report.model_dump(mode="json")) + report_sha256 = hashlib.sha256(report_bytes).hexdigest() + try: + micros = Decimal(str(round(float(report.est_model_cost_usd), 6))) * Decimal( + 1_000_000 + ) + except (InvalidOperation, TypeError, ValueError) as exc: + raise ProductionTerminalVerificationError( + "terminal report cost is not an exact integer microusd value" + ) from exc + if micros != micros.to_integral_value(): + raise ProductionTerminalVerificationError( + "terminal report cost is not an exact integer microusd value" + ) + required = envelope.required_contracts + passed = envelope.passed_contracts + confirmed_tiers = [ + int(item.verification_tier) + for result in report.results + for item in result.effect_evidence + if item.final_verdict == "confirmed" + and item.observed_effect == "present" + and item.verification_tier is not None + ] + if int(passed.effect) > 0 and len(confirmed_tiers) != int(passed.effect): + raise ProductionTerminalVerificationError( + "terminal report lacks exact confirmed effect tiers" + ) + effect_tier_reached = ( + { + 1: "independent_system", + 2: "independent_session", + 3: "persisted_state_reacquisition", + }.get(max(confirmed_tiers)) + if confirmed_tiers + else "none" + ) + if effect_tier_reached is None: + raise ProductionTerminalVerificationError( + "terminal report effect tier is outside the production range" + ) + try: + return ProductionRunReceipt.model_validate( + { + "source_schema_version": "openadapt.run-report/v1", + "outcome": report.execution_outcome, + "transaction_outcome": transaction_outcome.value, + "profile": report.execution_profile, + "production_eligible": False, + "steps_total": len(report.results), + "steps_ok": sum(1 for result in report.results if result.ok), + "heals": int(report.heal_count), + "model_calls": int(report.model_calls), + "est_cost_microusd": int(micros), + "duration_ms": int(round(float(report.total_ms))), + "rung_histogram": dict(report.rung_counts), + "evidence_classes": tuple(sorted(envelope.evidence_classes)), + "effect_tier_reached": effect_tier_reached, + "authorization_required": int(required.authorization), + "authorization_confirmed": int(passed.authorization), + "identity_required": int(required.identity), + "identity_confirmed": int(passed.identity), + "postconditions_required": int(required.postcondition), + "postconditions_confirmed": int(passed.postcondition), + "effects_required": int(required.effect), + "effects_confirmed": int(passed.effect), + "identity_armed": int(report.identity_armed_steps), + "identity_applicable": int(report.identity_applicable_steps), + "over_halt_count": _over_halt_count(report), + "substrate": report.execution_target_kind, + "provenance": "production", + "receipt_builder_version": _receipt_builder_version(), + "external_network_calls": envelope.external_network_calls, + "bundle_digest": report.bundle_content_digest, + "source_receipt_digest": report_sha256, + "source_receipt_sha256": report_sha256, + "generated_at": _hour_utc(report.started_at), + } + ) + except (TypeError, ValueError) as exc: + raise ProductionTerminalVerificationError( + "terminal report cannot produce a closed non-success receipt" + ) from exc + + def prepare_production_terminal_evidence( report: RunReport, ) -> PreparedProductionTerminalEvidence: - """Revalidate the exact report and build the only terminal-safe projections. + """Revalidate the exact report and build its terminal-safe projections. The returned report bytes are the bytes that the runner must store as the immutable report object. The production proof later binds the storage @@ -404,12 +647,42 @@ def prepare_production_terminal_evidence( envelope is None or validated.run_id_sha256 is None or validated.bundle_content_digest is None + or validated.workflow_contract_sha256 is None ): raise ProductionTerminalVerificationError( "terminal report lacks its run or bundle binding" ) + transaction_outcome = classify_transaction_outcome(validated) + if transaction_outcome not in { + TransactionOutcome.VERIFIED, + TransactionOutcome.HALTED_BEFORE_EFFECT, + TransactionOutcome.RECONCILIATION_REQUIRED, + }: + raise ProductionTerminalVerificationError( + "terminal report does not have an admitted terminal transaction outcome" + ) + if ( + validated.transaction_outcome != transaction_outcome.value + or validated.transaction_billable is not transaction_outcome.is_billable + ): + raise ProductionTerminalVerificationError( + "terminal report transaction fields differ from its retained evidence" + ) + if transaction_outcome is TransactionOutcome.HALTED_BEFORE_EFFECT and any( + _is_consequential_result(result) and not _effect_absence_proven(result) + for result in validated.results + ): + raise ProductionTerminalVerificationError( + "terminal HALTED report lacks exact effect-absence evidence" + ) try: - receipt = project_production_run_receipt(build_receipt(validated)) + if transaction_outcome is TransactionOutcome.VERIFIED: + receipt = project_production_run_receipt(build_receipt(validated)) + else: + receipt = project_production_terminal_run_receipt( + validated, + transaction_outcome=transaction_outcome, + ) outcome = ProductionExecutionOutcome.model_validate( { "version": envelope.version, @@ -435,7 +708,7 @@ def prepare_production_terminal_evidence( ) except (ReceiptError, ValueError) as exc: raise ProductionTerminalVerificationError( - "terminal report is not a complete production VERIFIED outcome" + "terminal report is not a complete production terminal outcome" ) from exc report_bytes = canonical_json(validated.model_dump(mode="json")) report_sha256 = hashlib.sha256(report_bytes).hexdigest() @@ -448,6 +721,7 @@ def prepare_production_terminal_evidence( execution_outcome=outcome, run_receipt=receipt, run_receipt_sha256=receipt_sha256, + transaction_outcome=transaction_outcome, report=validated, ) @@ -538,7 +812,7 @@ class ProductionIdentitySignal(ClosedSignedModel): class ProductionIdentityResult(ClosedSignedModel): result_index: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) - status: Literal["verified"] + status: Literal["verified", "mismatch", "abstain", "unreadable"] mode: Literal["context", "param", "structured", "pixel", "vlm", "signal_quorum"] signals: tuple[ProductionIdentitySignal, ...] = Field(max_length=32) @@ -559,17 +833,17 @@ class ProductionIdentityEvidenceManifest(ClosedSignedModel): ) identity_contract_sha256: str = Field(pattern=_SHA256_RE) workflow_contract_sha256: str = Field(pattern=_SHA256_RE) - required: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - confirmed: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - results: tuple[ProductionIdentityResult, ...] = Field( - min_length=1, max_length=10_000 - ) + required: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + confirmed: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + results: tuple[ProductionIdentityResult, ...] = Field(max_length=10_000) manifest_sha256: str = Field(pattern=_SHA256_RE) @model_validator(mode="after") def _digest(self) -> "ProductionIdentityEvidenceManifest": - if self.required != self.confirmed or len(self.results) != self.required: - raise ValueError("production identity evidence coverage is incomplete") + if self.confirmed > self.required or len(self.results) > self.required: + raise ValueError("production identity evidence coverage is invalid") + if sum(item.status == "verified" for item in self.results) != self.confirmed: + raise ValueError("production identity confirmed count is invalid") indices = tuple(item.result_index for item in self.results) if indices != tuple(sorted(indices)) or len(indices) != len(set(indices)): raise ValueError("production identity evidence results are invalid") @@ -586,23 +860,22 @@ class ProductionPostconditionEvidenceManifest(ClosedSignedModel): "openadapt.production-postcondition-evidence/v1" ) workflow_contract_sha256: str = Field(pattern=_SHA256_RE) - required: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - confirmed: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - records: tuple[PostconditionContractEvidence, ...] = Field( - min_length=1, max_length=10_000 - ) + required: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + confirmed: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + records: tuple[PostconditionContractEvidence, ...] = Field(max_length=10_000) manifest_sha256: str = Field(pattern=_SHA256_RE) @model_validator(mode="after") def _digest(self) -> "ProductionPostconditionEvidenceManifest": - if self.required != self.confirmed or len(self.records) != self.required: - raise ValueError("production postcondition evidence coverage is incomplete") + if self.confirmed > self.required or len(self.records) != self.required: + raise ValueError("production postcondition evidence coverage is invalid") if any( - item.verdict != "passed" - or item.workflow_contract_sha256 != self.workflow_contract_sha256 + item.workflow_contract_sha256 != self.workflow_contract_sha256 for item in self.records ): raise ValueError("production postcondition evidence is invalid") + if sum(item.verdict == "passed" for item in self.records) != self.confirmed: + raise ValueError("production postcondition confirmed count is invalid") expected = _evidence_manifest_sha256( b"openadapt-production-postcondition-evidence-v1\0", self ) @@ -624,23 +897,98 @@ class ProductionEffectEvidence(ClosedSignedModel): reconciliation_actions: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) +class ProductionTerminalEffectState(ClosedSignedModel): + """Remote-safe state for one effect contract on a non-success terminal.""" + + record_kind: Literal["terminal_effect_state"] = "terminal_effect_state" + result_index: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + effect_contract_hash: str = Field(pattern=r"^sha256:[a-f0-9]{64}$") + attempt_state: Literal[ + "not_actuated", + "delivered", + "actuated_api", + "delivery_uncertain", + ] + observed_effect: Literal["present", "absent", "conflicting", "unknown"] + effect_verified: StrictBool + verification_performed: StrictBool + verifier_identity: str | None = Field( + default=None, pattern=r"^sha256:[a-f0-9]{64}$" + ) + verification_tier: StrictInt | None = Field(default=None, ge=1, le=3) + final_verdict: Literal["confirmed", "refuted", "indeterminate"] | None + resolved_delivery_uncertainty: StrictBool + absence_basis: Literal["not_actuated", "verifier_refuted", "none"] + reconciliation_completed: StrictBool + reconciliation_actions: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + + @model_validator(mode="after") + def _closed_terminal_effect_state(self) -> "ProductionTerminalEffectState": + if self.verification_performed != (self.final_verdict is not None): + raise ValueError("terminal effect verification fields are inconsistent") + if self.verification_performed != (self.verification_tier is not None): + raise ValueError("terminal effect verification tier is inconsistent") + if self.verification_performed != (self.verifier_identity is not None): + raise ValueError("terminal effect verifier identity is inconsistent") + if self.effect_verified != ( + self.final_verdict == "confirmed" and self.observed_effect == "present" + ): + raise ValueError("terminal effect verified state is inconsistent") + if self.absence_basis == "not_actuated": + if ( + self.attempt_state != "not_actuated" + or self.observed_effect != "absent" + or self.verification_performed + or self.resolved_delivery_uncertainty + ): + raise ValueError("terminal non-actuation evidence is invalid") + elif self.absence_basis == "verifier_refuted": + if ( + self.attempt_state == "not_actuated" + or self.observed_effect != "absent" + or self.final_verdict != "refuted" + or not self.verification_performed + ): + raise ValueError("terminal verifier absence evidence is invalid") + elif self.attempt_state == "not_actuated" or ( + self.final_verdict == "refuted" and self.observed_effect == "absent" + ): + raise ValueError("terminal absence evidence must name its basis") + if self.reconciliation_completed != (self.reconciliation_actions > 0): + raise ValueError("terminal effect reconciliation fields are inconsistent") + return self + + class ProductionEffectEvidenceManifest(ClosedSignedModel): schema_version: Literal["openadapt.production-effect-evidence/v1"] = ( "openadapt.production-effect-evidence/v1" ) effect_contract_sha256: str = Field(pattern=_SHA256_RE) workflow_contract_sha256: str = Field(pattern=_SHA256_RE) - required: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - confirmed: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - records: tuple[ProductionEffectEvidence, ...] = Field( - min_length=1, max_length=10_000 + required: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + confirmed: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + records: tuple[ProductionEffectEvidence | ProductionTerminalEffectState, ...] = ( + Field(max_length=10_000) ) manifest_sha256: str = Field(pattern=_SHA256_RE) @model_validator(mode="after") def _digest(self) -> "ProductionEffectEvidenceManifest": - if self.required != self.confirmed or len(self.records) != self.required: - raise ValueError("production effect evidence coverage is incomplete") + if self.confirmed > self.required or len(self.records) != self.required: + raise ValueError("production effect evidence coverage is invalid") + if all(isinstance(item, ProductionEffectEvidence) for item in self.records): + if self.required != self.confirmed: + raise ValueError("production effect success coverage is incomplete") + elif ( + sum( + isinstance(item, ProductionTerminalEffectState) + and item.final_verdict == "confirmed" + and item.observed_effect == "present" + for item in self.records + ) + != self.confirmed + ): + raise ValueError("production effect confirmed count is invalid") keys = tuple( (item.result_index, item.effect_contract_hash) for item in self.records ) @@ -1196,9 +1544,7 @@ class ProductionDeliveryPermitChain(ClosedSignedModel): schema_version: Literal["openadapt.production-delivery-permit-chain/v2"] = ( PERMIT_CHAIN_SCHEMA ) - entries: tuple[ProductionDeliveryPermit, ...] = Field( - min_length=1, max_length=10_000 - ) + entries: tuple[ProductionDeliveryPermit, ...] = Field(max_length=10_000) permit_chain_sha256: str = Field(pattern=_SHA256_RE) @model_validator(mode="after") @@ -1208,6 +1554,19 @@ def _closed_chain(self) -> "ProductionDeliveryPermitChain": # in-memory mutation cannot enter a trusted chain with a stale digest. for item in self.entries: ProductionDeliveryPermit.model_validate(item.model_dump(mode="json")) + expected = hashlib.sha256( + PERMIT_CHAIN_DOMAIN + + canonical_json( + { + "schema_version": self.schema_version, + "entries": [item.model_dump(mode="json") for item in self.entries], + } + ) + ).hexdigest() + if self.permit_chain_sha256 != expected: + raise ValueError("delivery permit chain digest is invalid") + if not self.entries: + return self authority = self.entries[0].execution_authority_id authority_digest = self.entries[0].execution_authority_sha256 admission_digest = self.entries[0].admission_artifact_sha256 @@ -1281,17 +1640,6 @@ def _closed_chain(self) -> "ProductionDeliveryPermitChain": for previous, current in zip(event_times, event_times[1:]) ): raise ValueError("delivery permit chronology is invalid") - expected = hashlib.sha256( - PERMIT_CHAIN_DOMAIN - + canonical_json( - { - "schema_version": self.schema_version, - "entries": [item.model_dump(mode="json") for item in self.entries], - } - ) - ).hexdigest() - if self.permit_chain_sha256 != expected: - raise ValueError("delivery permit chain digest is invalid") return self @classmethod @@ -1410,25 +1758,109 @@ def build_production_evidence_manifests( confirmed=outcome.passed_contracts.postcondition, records=outcome.postcondition_evidence, ) - effect_records: list[ProductionEffectEvidence] = [] + effect_records: list[ProductionEffectEvidence | ProductionTerminalEffectState] = [] for result_index, result in enumerate(report.results): if result.skipped or result.exception_handled: continue - for item in result.effect_evidence: + if prepared.transaction_outcome is TransactionOutcome.VERIFIED: + for effect_item in result.effect_evidence: + effect_records.append( + ProductionEffectEvidence.model_validate( + { + "result_index": result_index, + "effect_contract_hash": effect_item.effect_contract_hash, + "verifier_identity": effect_item.verifier_identity, + "verification_tier": effect_item.verification_tier, + "final_verdict": effect_item.final_verdict, + "observed_effect": effect_item.observed_effect, + "reconciliation_completed": ( + effect_item.reconciliation_completed + ), + "reconciliation_actions": ( + effect_item.reconciliation_actions + ), + } + ) + ) + continue + if not _is_consequential_result(result): + continue + evidence_by_hash: dict[str, list[EffectVerificationEvidence]] = {} + for effect_item in result.effect_evidence: + evidence_by_hash.setdefault(effect_item.effect_contract_hash, []).append( + effect_item + ) + effect_hashes = tuple(sorted(set(result.effect_contract_hashes))) + if len(effect_hashes) != len(result.effect_contract_hashes): + raise ProductionTerminalVerificationError( + "terminal effect contract hashes are duplicated" + ) + attempt_state = _attempt_state(result) + uncertainty_resolved = bool( + result.delivery_uncertainty is not None + and result.delivery_uncertainty.resolved_by_contract + ) + for effect_hash in effect_hashes: + matches = evidence_by_hash.pop(effect_hash, []) + if len(matches) > 1: + raise ProductionTerminalVerificationError( + "terminal effect evidence is duplicated" + ) + terminal_item: EffectVerificationEvidence | None = ( + matches[0] if matches else None + ) + if terminal_item is None: + observed_effect = ( + "absent" if attempt_state == "not_actuated" else "unknown" + ) + final_verdict = None + verification_tier = None + verifier_identity = None + reconciliation_completed = False + reconciliation_actions = 0 + absence_basis = ( + "not_actuated" if attempt_state == "not_actuated" else "none" + ) + else: + observed_effect = terminal_item.observed_effect + final_verdict = terminal_item.final_verdict + verification_tier = terminal_item.verification_tier + verifier_identity = terminal_item.verifier_identity + reconciliation_completed = terminal_item.reconciliation_completed + reconciliation_actions = terminal_item.reconciliation_actions + absence_basis = ( + "verifier_refuted" + if terminal_item.final_verdict == "refuted" + and terminal_item.observed_effect == "absent" + else "none" + ) effect_records.append( - ProductionEffectEvidence.model_validate( + ProductionTerminalEffectState.model_validate( { "result_index": result_index, - "effect_contract_hash": item.effect_contract_hash, - "verifier_identity": item.verifier_identity, - "verification_tier": item.verification_tier, - "final_verdict": item.final_verdict, - "observed_effect": item.observed_effect, - "reconciliation_completed": item.reconciliation_completed, - "reconciliation_actions": item.reconciliation_actions, + "effect_contract_hash": effect_hash, + "attempt_state": attempt_state, + "observed_effect": observed_effect, + "effect_verified": bool( + terminal_item is not None + and terminal_item.final_verdict == "confirmed" + and terminal_item.observed_effect == "present" + ), + "verification_performed": terminal_item is not None, + "verifier_identity": verifier_identity, + "verification_tier": verification_tier, + "final_verdict": final_verdict, + "resolved_delivery_uncertainty": uncertainty_resolved, + "absence_basis": absence_basis, + "reconciliation_completed": reconciliation_completed, + "reconciliation_actions": reconciliation_actions, } ) ) + if evidence_by_hash: + raise ProductionTerminalVerificationError( + "terminal effect evidence lacks a declared contract" + ) effect_records.sort(key=lambda item: (item.result_index, item.effect_contract_hash)) effect = build_evidence_manifest( ProductionEffectEvidenceManifest, @@ -1482,7 +1914,7 @@ class ProductionTerminalVerificationPayload(ClosedSignedModel): execution_authority_sha256: str = Field(pattern=_SHA256_RE) execution_authority_signer_sha256: str = Field(pattern=_SHA256_RE) permit_chain: ProductionDeliveryPermitChain - permit_count: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) + permit_count: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) final_authority_sequence: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) final_runtime_delivery_sequence: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) workflow_contract_sha256: str = Field(pattern=_SHA256_RE) @@ -1498,7 +1930,7 @@ class ProductionTerminalVerificationPayload(ClosedSignedModel): issued_at: str @model_validator(mode="after") - def _closed_terminal_success(self) -> "ProductionTerminalVerificationPayload": + def _closed_terminal_outcome(self) -> "ProductionTerminalVerificationPayload": if hashlib.sha256(self.run_id.encode("utf-8")).hexdigest() != ( self.flow_run_id_sha256 ): @@ -1508,49 +1940,63 @@ def _closed_terminal_success(self) -> "ProductionTerminalVerificationPayload": if self.workflow_version_id != self.bundle_version_id: raise ValueError("terminal workflow and bundle versions must match") chain = self.permit_chain - final = chain.entries[-1] - if ( - self.execution_authority_id, - self.execution_authority_sha256, - self.execution_authority_signer_sha256, - self.admission_artifact_sha256, - ) != ( - final.execution_authority_id, - final.execution_authority_sha256, - final.authority_signer_sha256, - final.admission_artifact_sha256, - ): - raise ValueError("terminal authority does not match its permit chain") - if ( - self.run_id != final.run_id - or self.flow_run_id_sha256 != final.flow_run_id_sha256 - ): - raise ValueError("terminal run identity does not match its permit chain") - if ( - self.evidence_identity_sha256, - self.environment_digest, - self.qualification_signer_registry_sha256, - self.qualification_signer_registry_revision, - ) != ( - final.evidence_identity_sha256, - final.environment_digest, - final.qualification_signer_registry_sha256, - final.qualification_signer_registry_revision, - ): - raise ValueError("terminal qualification state does not match its permits") - if ( - self.permit_count, - self.final_authority_sequence, - self.final_runtime_delivery_sequence, - ) != ( - len(chain.entries), - final.authority_sequence, - final.runtime_delivery_sequence, - ): - raise ValueError("terminal permit counts do not match the permit chain") receipt = self.run_receipt outcome = self.execution_outcome manifests = self.evidence_manifests + if chain.entries: + final = chain.entries[-1] + if ( + self.execution_authority_id, + self.execution_authority_sha256, + self.execution_authority_signer_sha256, + self.admission_artifact_sha256, + ) != ( + final.execution_authority_id, + final.execution_authority_sha256, + final.authority_signer_sha256, + final.admission_artifact_sha256, + ): + raise ValueError("terminal authority does not match its permit chain") + if ( + self.run_id != final.run_id + or self.flow_run_id_sha256 != final.flow_run_id_sha256 + ): + raise ValueError( + "terminal run identity does not match its permit chain" + ) + if ( + self.evidence_identity_sha256, + self.environment_digest, + self.qualification_signer_registry_sha256, + self.qualification_signer_registry_revision, + ) != ( + final.evidence_identity_sha256, + final.environment_digest, + final.qualification_signer_registry_sha256, + final.qualification_signer_registry_revision, + ): + raise ValueError( + "terminal qualification state does not match its permits" + ) + if ( + self.permit_count, + self.final_authority_sequence, + self.final_runtime_delivery_sequence, + ) != ( + len(chain.entries), + final.authority_sequence, + final.runtime_delivery_sequence, + ): + raise ValueError("terminal permit counts do not match the permit chain") + elif ( + receipt.transaction_outcome != "HALTED_BEFORE_EFFECT" + or self.permit_count != 0 + or self.final_authority_sequence != 0 + or self.final_runtime_delivery_sequence != 0 + ): + raise ValueError( + "only a HALTED_BEFORE_EFFECT terminal may use an empty permit chain" + ) if ( self.workflow_contract_sha256 != outcome.workflow_contract_sha256 or self.execution_outcome_sha256 != outcome.artifact_sha256() @@ -1622,17 +2068,56 @@ def _closed_terminal_success(self) -> "ProductionTerminalVerificationPayload": raise ValueError( "terminal run receipt does not match its execution outcome" ) + transaction_outcome = receipt.transaction_outcome + if transaction_outcome == "VERIFIED": + if ( + outcome.outcome != "VERIFIED" + or not outcome.production_eligible + or not outcome.execution_completed + or not chain.entries + ): + raise ValueError("terminal VERIFIED proof is incomplete") + elif transaction_outcome == "HALTED_BEFORE_EFFECT": + if ( + outcome.outcome != "HALTED" + or outcome.production_eligible + or outcome.execution_completed + or receipt.production_eligible + or any( + not isinstance(item, ProductionTerminalEffectState) + or item.absence_basis not in {"not_actuated", "verifier_refuted"} + for item in manifests.effect.records + ) + ): + raise ValueError("terminal HALTED proof lacks exact effect absence") + elif ( + transaction_outcome != "RECONCILIATION_REQUIRED" + or outcome.outcome == "VERIFIED" + or outcome.production_eligible + or receipt.production_eligible + or not chain.entries + ): + raise ValueError("terminal reconciliation proof is invalid") + if transaction_outcome != "VERIFIED" and ( + receipt.source_receipt_digest != self.run_report_sha256 + or receipt.source_receipt_sha256 != self.run_report_sha256 + ): + raise ValueError("terminal non-success receipt does not bind its report") verified = _parse_utc(self.verified_at, field="terminal verified_at") issued = _parse_utc(self.issued_at, field="terminal issued_at") if not verified <= issued <= verified + timedelta(minutes=5): raise ValueError("terminal proof issue time is invalid") - final_delivered = _parse_utc(final.delivered_at, field="permit delivered_at") - registry_expires = _parse_utc( - final.qualification_signer_registry_expires_at, - field="permit registry expires_at", - ) - if not final_delivered <= verified < registry_expires: - raise ValueError("terminal verification is outside permit chronology") + if chain.entries: + final = chain.entries[-1] + final_delivered = _parse_utc( + final.delivered_at, field="permit delivered_at" + ) + registry_expires = _parse_utc( + final.qualification_signer_registry_expires_at, + field="permit registry expires_at", + ) + if not final_delivered <= verified < registry_expires: + raise ValueError("terminal verification is outside permit chronology") if self.run_report_object_sha256 != self.run_report_sha256: raise ValueError( "terminal report object must contain the exact revalidated report bytes" @@ -1734,14 +2219,12 @@ class ProductionTerminalVerificationExpected(ClosedSignedModel): execution_authority_sha256: str = Field(pattern=_SHA256_RE) execution_authority_signer_sha256: str = Field(pattern=_SHA256_RE) permit_chain_sha256: str = Field(pattern=_SHA256_RE) - permit_count: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) + permit_count: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) final_authority_sequence: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) final_runtime_delivery_sequence: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) authenticated_runner_id_sha256: str = Field(pattern=_SHA256_RE) authenticated_session_id_sha256: str = Field(pattern=_SHA256_RE) - acknowledged_one_use_claim_ids: tuple[str, ...] = Field( - min_length=1, max_length=10_000 - ) + acknowledged_one_use_claim_ids: tuple[str, ...] = Field(max_length=10_000) workflow_contract_sha256: str = Field(pattern=_SHA256_RE) execution_outcome_sha256: str = Field(pattern=_SHA256_RE) run_receipt_sha256: str = Field(pattern=_SHA256_RE) @@ -1885,7 +2368,7 @@ def build_production_terminal_verification( execution_authority_sha256=context.execution_authority_sha256, permit_chain=context.permit_chain, ) - final = context.permit_chain.entries[-1] + final = context.permit_chain.entries[-1] if context.permit_chain.entries else None payload = ProductionTerminalVerificationPayload( run_id=context.run_id, flow_run_id_sha256=prepared.flow_run_id_sha256, @@ -1919,8 +2402,10 @@ def build_production_terminal_verification( execution_authority_signer_sha256=(context.execution_authority_signer_sha256), permit_chain=context.permit_chain, permit_count=len(context.permit_chain.entries), - final_authority_sequence=final.authority_sequence, - final_runtime_delivery_sequence=final.runtime_delivery_sequence, + final_authority_sequence=(final.authority_sequence if final is not None else 0), + final_runtime_delivery_sequence=( + final.runtime_delivery_sequence if final is not None else 0 + ), workflow_contract_sha256=(prepared.execution_outcome.workflow_contract_sha256), execution_outcome=prepared.execution_outcome, execution_outcome_sha256=prepared.execution_outcome.artifact_sha256(), diff --git a/tests/fixtures/terminal_verification_v2_terminal_vectors.json b/tests/fixtures/terminal_verification_v2_terminal_vectors.json new file mode 100644 index 00000000..5764f76e --- /dev/null +++ b/tests/fixtures/terminal_verification_v2_terminal_vectors.json @@ -0,0 +1 @@ +{"key_id":"evidence-runner-ed25519-56475aa75463474c","private_key_base64":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","public_key_base64":"A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=","schema_version":"openadapt.production-terminal-cross-language-vectors/v2","signature_domain_base64":"b3BlbmFkYXB0LXByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uLXYyAA==","signer_sha256":"56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c","vectors":[{"callback":{"artifact_bytes_source":"envelope_canonical_base64","outcome":"HALTED_BEFORE_EFFECT","report_sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","run_id":"00000000-0000-4000-8000-000000000001","schema_version":"openadapt.hosted-runner-terminal/v1","started":true,"uncertain_delivery":false},"effect_state":{"absence_basis":"not_actuated","attempt_state":"not_actuated","effect_contract_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","effect_verified":false,"final_verdict":null,"observed_effect":"absent","reconciliation_actions":0,"reconciliation_completed":false,"record_kind":"terminal_effect_state","resolved_delivery_uncertainty":false,"result_index":0,"verification_performed":false,"verification_tier":null,"verifier_identity":null},"envelope_canonical_base64":"eyJwYXlsb2FkIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6IjMyY2QzNjEzYmVlOWJjNjkwNjkzYTM5NWQ5OTMzNTQ3ZjllZWI5ZjZmYWE1ZGJkYmIxNGExMDYyOThiZTQ2NWMiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiODJkYWI1NDI4ZTI1MmQxOGE1OWIwMTc3NzM5ZTAwY2FhZGI3NjJkY2Y1OWY0MDM2ZGVhNGQ3N2MzYzMyMDNjYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6ImViYjU4MzYxNzhlZGZhYzYyOWJkOWJjYTkwZWNkMDE5NTkwOTQ5NjZhMGRkNjNiYWVhN2E2MmY2ZTNiMDRmZjgiLCJyZWNvcmRzIjpbeyJhYnNlbmNlX2Jhc2lzIjoibm90X2FjdHVhdGVkIiwiYXR0ZW1wdF9zdGF0ZSI6Im5vdF9hY3R1YXRlZCIsImVmZmVjdF9jb250cmFjdF9oYXNoIjoic2hhMjU2OmFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJlZmZlY3RfdmVyaWZpZWQiOmZhbHNlLCJmaW5hbF92ZXJkaWN0IjpudWxsLCJvYnNlcnZlZF9lZmZlY3QiOiJhYnNlbnQiLCJyZWNvbmNpbGlhdGlvbl9hY3Rpb25zIjowLCJyZWNvbmNpbGlhdGlvbl9jb21wbGV0ZWQiOmZhbHNlLCJyZWNvcmRfa2luZCI6InRlcm1pbmFsX2VmZmVjdF9zdGF0ZSIsInJlc29sdmVkX2RlbGl2ZXJ5X3VuY2VydGFpbnR5IjpmYWxzZSwicmVzdWx0X2luZGV4IjowLCJ2ZXJpZmljYXRpb25fcGVyZm9ybWVkIjpmYWxzZSwidmVyaWZpY2F0aW9uX3RpZXIiOm51bGwsInZlcmlmaWVyX2lkZW50aXR5IjpudWxsfV0sInJlcXVpcmVkIjoxLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWVmZmVjdC1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiaWRlbnRpdHkiOnsiY29uZmlybWVkIjoxLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiMTM1Yzg2M2VkYzdmYWIxOGNhMjliNDkyOTNmYzI1YTFjOWJkNGRlMGU5ZWQyOGQ0Y2VjNmU4ZDBhNjc5MTIyOSIsInJlcXVpcmVkIjoxLCJyZXN1bHRzIjpbeyJtb2RlIjoic3RydWN0dXJlZCIsInJlc3VsdF9pbmRleCI6MCwic2lnbmFscyI6W10sInN0YXR1cyI6InZlcmlmaWVkIn1dLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWlkZW50aXR5LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJwb2xpY3kiOnsiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX3BvbGljeV9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZ292ZXJuZWRfcnVudGltZV9pbnB1dHNfZGlnZXN0IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiI0Zjk4OGQwMTViY2Q3Y2Q3ZTA1YmEwNjYzZDg5ZjBhOTY4ZjBkOGFjZTk5YjA5MGViYWJlZTUyYTFjNTUwNDIxIiwibWluaW11bV9lZmZlY3RfdGllciI6MSwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb2xpY3ktZXZpZGVuY2UvdjEifSwicG9zdGNvbmRpdGlvbiI6eyJjb25maXJtZWQiOjAsIm1hbmlmZXN0X3NoYTI1NiI6IjNlYTRjNjRhMzBiMGNmMTdiY2EwNGYzMjYwZTBhNDcxOGIyN2Y1MWUxZmRjMmJiODhhMTgyNjMwYTM0ZGQ1ZmYiLCJyZWNvcmRzIjpbeyJhY3Rpb25fa2luZCI6ImNsaWNrIiwiYWN0dWF0aW9uX3BhdGgiOiJndWkiLCJjb250cmFjdF9pbmRleCI6MCwiY29udHJhY3Rfa2luZCI6ImV4cGxpY2l0X3ByZWRpY2F0ZSIsImNvbnRyYWN0X3NoYTI1NiI6IjBhZGUwYjUyMGJjMDA3ODkxY2FjY2EwMDE3MTU3ODQ0MmEyYTc0Y2VmMTVmZjY0ZGRlNDVkZjA3YWZiNjRhZWIiLCJyZXN1bHRfaW5kZXgiOjAsInN0ZXBfY29udHJhY3Rfc2hhMjU2IjoiMWE1OTUzNmE0ODhiMWM0NmFhY2YwYmEzNzkzMGI4NDA2MmMxNDI3NTVmYTkzOTE1MmVmNWUyOGM0MDk0NzFlZCIsInN0ZXBfaW5kZXgiOjAsInZlcmRpY3QiOiJ1bnZlcmlmaWFibGUiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn1dLCJyZXF1aXJlZCI6MSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb3N0Y29uZGl0aW9uLWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9fSwiZXZpZGVuY2VfcnVubmVyX3NpZ25lcl9zaGEyNTYiOiI1NjQ3NWFhNzU0NjM0NzRjMDI4NWRmNWRiZjJiY2FiNzNkYTY1MTM1ODgzOWU5Yjc3NDgxYjJlYWIxMDc3MDhjIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2lnbmVyX3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJleGVjdXRpb25fb3V0Y29tZSI6eyJjb21wZW5zYXRpb25fYWN0aW9ucyI6MCwiZXZpZGVuY2VfY2xhc3NlcyI6WyJhdXRob3JpemF0aW9uIiwiaWRlbnRpdHkiXSwiZXhlY3V0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJub25lIiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJwYXNzZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjEsInBvc3Rjb25kaXRpb24iOjB9LCJwb3N0Y29uZGl0aW9uX2V2aWRlbmNlIjpbeyJhY3Rpb25fa2luZCI6ImNsaWNrIiwiYWN0dWF0aW9uX3BhdGgiOiJndWkiLCJjb250cmFjdF9pbmRleCI6MCwiY29udHJhY3Rfa2luZCI6ImV4cGxpY2l0X3ByZWRpY2F0ZSIsImNvbnRyYWN0X3NoYTI1NiI6IjBhZGUwYjUyMGJjMDA3ODkxY2FjY2EwMDE3MTU3ODQ0MmEyYTc0Y2VmMTVmZjY0ZGRlNDVkZjA3YWZiNjRhZWIiLCJyZXN1bHRfaW5kZXgiOjAsInN0ZXBfY29udHJhY3Rfc2hhMjU2IjoiMWE1OTUzNmE0ODhiMWM0NmFhY2YwYmEzNzkzMGI4NDA2MmMxNDI3NTVmYTkzOTE1MmVmNWUyOGM0MDk0NzFlZCIsInN0ZXBfaW5kZXgiOjAsInZlcmRpY3QiOiJ1bnZlcmlmaWFibGUiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn1dLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjpmYWxzZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicXVhbGlmaWNhdGlvbl9ldmlkZW5jZV9vbmx5IjpmYWxzZSwicmVxdWlyZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MSwiaWRlbnRpdHkiOjEsInBvc3Rjb25kaXRpb24iOjF9LCJ2ZXJzaW9uIjoib3BlbmFkYXB0LmV4ZWN1dGlvbi1vdXRjb21lL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJleGVjdXRpb25fb3V0Y29tZV9zaGEyNTYiOiJmZTM4OWJhYjBlYjQ0NDdjZWMxOTk4Mjc0ZjcyN2NhNTcwMDQ5ZmVmZDU1N2RiNzk5MDQ3Njk5ZjJmNzhmMTQyIiwiZXhlY3V0aW9uX3B1cnBvc2UiOiJwcm9kdWN0aW9uIiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjowLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDNaIiwicGVybWl0X2NoYWluIjp7ImVudHJpZXMiOltdLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiODJkYWI1NDI4ZTI1MmQxOGE1OWIwMTc3NzM5ZTAwY2FhZGI3NjJkY2Y1OWY0MDM2ZGVhNGQ3N2MzYzMyMDNjYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWNoYWluL3YyIn0sInBlcm1pdF9jb3VudCI6MCwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVjZWlwdCI6eyJhdXRob3JpemF0aW9uX2NvbmZpcm1lZCI6MSwiYXV0aG9yaXphdGlvbl9yZXF1aXJlZCI6MSwiYnVuZGxlX2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJkdXJhdGlvbl9tcyI6MTAwLCJlZmZlY3RfdGllcl9yZWFjaGVkIjoibm9uZSIsImVmZmVjdHNfY29uZmlybWVkIjowLCJlZmZlY3RzX3JlcXVpcmVkIjoxLCJlc3RfY29zdF9taWNyb3VzZCI6MCwiZXZpZGVuY2VfY2xhc3NlcyI6WyJhdXRob3JpemF0aW9uIiwiaWRlbnRpdHkiXSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJnZW5lcmF0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImhlYWxzIjowLCJpZGVudGl0eV9hcHBsaWNhYmxlIjoxLCJpZGVudGl0eV9hcm1lZCI6MSwiaWRlbnRpdHlfY29uZmlybWVkIjoxLCJpZGVudGl0eV9yZXF1aXJlZCI6MSwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJvdmVyX2hhbHRfY291bnQiOjAsInBvc3Rjb25kaXRpb25zX2NvbmZpcm1lZCI6MCwicG9zdGNvbmRpdGlvbnNfcmVxdWlyZWQiOjEsInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJwcm92ZW5hbmNlIjoicHJvZHVjdGlvbiIsInJlY2VpcHRfYnVpbGRlcl92ZXJzaW9uIjoiMS4yLjMiLCJydW5nX2hpc3RvZ3JhbSI6e30sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcnVuLXJlY2VpcHQvdjEiLCJzb3VyY2VfcmVjZWlwdF9kaWdlc3QiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3JlY2VpcHRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9zY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5ydW4tcmVwb3J0L3YxIiwic3RlcHNfb2siOjAsInN0ZXBzX3RvdGFsIjoxLCJzdWJzdHJhdGUiOiJ3ZWIiLCJ0cmFuc2FjdGlvbl9vdXRjb21lIjoiSEFMVEVEX0JFRk9SRV9FRkZFQ1QifSwicnVuX3JlY2VpcHRfc2hhMjU2IjoiYWI1MGEwMDZhODg4YjNmOTBlMWU3MWVmMjA5MjFhZjcxN2IyZjQxYTM2NzZlNjA1ZmJkYjgyMGNiNmI1M2NjNCIsInJ1bl9yZXBvcnRfb2JqZWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW5fcmVwb3J0X29iamVjdF92ZXJzaW9uIjoidmVyc2lvbjpoYWx0ZWQ6MSIsInJ1bl9yZXBvcnRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bnRpbWVfc3Vic3RyYXRlIjoid2ViIiwicnVudGltZV92YWxpZGF0aW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA1Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi10ZXJtaW5hbC12ZXJpZmljYXRpb24vdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9zZXF1ZW5jZSI6MSwidmVyaWZpZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJ3b3JrZmxvd19pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMyIsIndvcmtmbG93X3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQifSwic2lnbmF0dXJlIjoiNUptaHdlQXN0OGZ0eUwtbjd4a1JLNTE0cEJCbkU1TzlobUZ3RjRDbmtKOEtpVHBKeExFQWs2ZHNzTUhfTkVSM2QzbVFOeENBQkNHckRkY29tSkE5Q1EiLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImV2aWRlbmNlLXJ1bm5lci1lZDI1NTE5LTU2NDc1YWE3NTQ2MzQ3NGMiLCJwdWJsaWNfa2V5IjoiQTZFSHYvUE9FTDRkY04wWTUwdkFtV2ZrMWpDYnBRMWZIZHlHWkJKVk1iZz0ifX0=","name":"halted-before-effect-zero-permit","payload_canonical_sha256":"26f4073246b4998650636bd3f1cef10835c7f51d91ccfa1d0854bc67015e6792","signature":"5JmhweAst8ftyL-n7xkRK514pBBnE5O9hmFwF4CnkJ8KiTpJxLEAk6dssMH_NER3d3mQNxCABCGrDdcomJA9CQ","terminal_verification_artifact_sha256":"43f6fc7809d789aa42785b7f76ce2dd751b2c3251c8fc74ec1f7bfe3a70b6577"},{"callback":{"artifact_bytes_source":"envelope_canonical_base64","outcome":"RECONCILIATION_REQUIRED","report_sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","run_id":"00000000-0000-4000-8000-000000000001","schema_version":"openadapt.hosted-runner-terminal/v1","started":true,"uncertain_delivery":true},"effect_state":{"absence_basis":"none","attempt_state":"delivery_uncertain","effect_contract_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","effect_verified":false,"final_verdict":null,"observed_effect":"unknown","reconciliation_actions":0,"reconciliation_completed":false,"record_kind":"terminal_effect_state","resolved_delivery_uncertainty":false,"result_index":0,"verification_performed":false,"verification_tier":null,"verifier_identity":null},"envelope_canonical_base64":"eyJwYXlsb2FkIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6ImYwY2ExOWE2YjNlYWJlNTk0ZGJjOTA2ODljMmY3NGNiZTVlODJkNGQ5NDdkYjI5YTVkMDdiMTY0MWU4OTYxY2YiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6ImFmMTUwOWNjMmU5ZmExZjViOGI4MTVlN2IxZjYwNDYxOWIyYTM1ZWMwZGU1ZTBkMzRiYmZhNjdjYzkxZGJiYzciLCJyZWNvcmRzIjpbeyJhYnNlbmNlX2Jhc2lzIjoibm9uZSIsImF0dGVtcHRfc3RhdGUiOiJkZWxpdmVyeV91bmNlcnRhaW4iLCJlZmZlY3RfY29udHJhY3RfaGFzaCI6InNoYTI1NjphYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X3ZlcmlmaWVkIjpmYWxzZSwiZmluYWxfdmVyZGljdCI6bnVsbCwib2JzZXJ2ZWRfZWZmZWN0IjoidW5rbm93biIsInJlY29uY2lsaWF0aW9uX2FjdGlvbnMiOjAsInJlY29uY2lsaWF0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsInJlY29yZF9raW5kIjoidGVybWluYWxfZWZmZWN0X3N0YXRlIiwicmVzb2x2ZWRfZGVsaXZlcnlfdW5jZXJ0YWludHkiOmZhbHNlLCJyZXN1bHRfaW5kZXgiOjAsInZlcmlmaWNhdGlvbl9wZXJmb3JtZWQiOmZhbHNlLCJ2ZXJpZmljYXRpb25fdGllciI6bnVsbCwidmVyaWZpZXJfaWRlbnRpdHkiOm51bGx9XSwicmVxdWlyZWQiOjEsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZWZmZWN0LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJpZGVudGl0eSI6eyJjb25maXJtZWQiOjEsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiIxMzVjODYzZWRjN2ZhYjE4Y2EyOWI0OTI5M2ZjMjVhMWM5YmQ0ZGUwZTllZDI4ZDRjZWM2ZThkMGE2NzkxMjI5IiwicmVxdWlyZWQiOjEsInJlc3VsdHMiOlt7Im1vZGUiOiJzdHJ1Y3R1cmVkIiwicmVzdWx0X2luZGV4IjowLCJzaWduYWxzIjpbXSwic3RhdHVzIjoidmVyaWZpZWQifV0sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24taWRlbnRpdHktZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sInBvbGljeSI6eyJhZG1pc3Npb25fcG9saWN5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZ292ZXJuZWRfcG9saWN5X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJnb3Zlcm5lZF9ydW50aW1lX2lucHV0c19kaWdlc3QiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsIm1hbmlmZXN0X3NoYTI1NiI6IjRmOTg4ZDAxNWJjZDdjZDdlMDViYTA2NjNkODlmMGE5NjhmMGQ4YWNlOTliMDkwZWJhYmVlNTJhMWM1NTA0MjEiLCJtaW5pbXVtX2VmZmVjdF90aWVyIjoxLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvbGljeS1ldmlkZW5jZS92MSJ9LCJwb3N0Y29uZGl0aW9uIjp7ImNvbmZpcm1lZCI6MCwibWFuaWZlc3Rfc2hhMjU2IjoiM2VhNGM2NGEzMGIwY2YxN2JjYTA0ZjMyNjBlMGE0NzE4YjI3ZjUxZTFmZGMyYmI4OGExODI2MzBhMzRkZDVmZiIsInJlY29yZHMiOlt7ImFjdGlvbl9raW5kIjoiY2xpY2siLCJhY3R1YXRpb25fcGF0aCI6Imd1aSIsImNvbnRyYWN0X2luZGV4IjowLCJjb250cmFjdF9raW5kIjoiZXhwbGljaXRfcHJlZGljYXRlIiwiY29udHJhY3Rfc2hhMjU2IjoiMGFkZTBiNTIwYmMwMDc4OTFjYWNjYTAwMTcxNTc4NDQyYTJhNzRjZWYxNWZmNjRkZGU0NWRmMDdhZmI2NGFlYiIsInJlc3VsdF9pbmRleCI6MCwic3RlcF9jb250cmFjdF9zaGEyNTYiOiIxYTU5NTM2YTQ4OGIxYzQ2YWFjZjBiYTM3OTMwYjg0MDYyYzE0Mjc1NWZhOTM5MTUyZWY1ZTI4YzQwOTQ3MWVkIiwic3RlcF9pbmRleCI6MCwidmVyZGljdCI6InVudmVyaWZpYWJsZSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifV0sInJlcXVpcmVkIjoxLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvc3Rjb25kaXRpb24tZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn19LCJldmlkZW5jZV9ydW5uZXJfc2lnbmVyX3NoYTI1NiI6IjU2NDc1YWE3NTQ2MzQ3NGMwMjg1ZGY1ZGJmMmJjYWI3M2RhNjUxMzU4ODM5ZTliNzc0ODFiMmVhYjEwNzcwOGMiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImV4ZWN1dGlvbl9vdXRjb21lIjp7ImNvbXBlbnNhdGlvbl9hY3Rpb25zIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iLCJpZGVudGl0eSJdLCJleGVjdXRpb25fY29tcGxldGVkIjpmYWxzZSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsInBhc3NlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjowLCJpZGVudGl0eSI6MSwicG9zdGNvbmRpdGlvbiI6MH0sInBvc3Rjb25kaXRpb25fZXZpZGVuY2UiOlt7ImFjdGlvbl9raW5kIjoiY2xpY2siLCJhY3R1YXRpb25fcGF0aCI6Imd1aSIsImNvbnRyYWN0X2luZGV4IjowLCJjb250cmFjdF9raW5kIjoiZXhwbGljaXRfcHJlZGljYXRlIiwiY29udHJhY3Rfc2hhMjU2IjoiMGFkZTBiNTIwYmMwMDc4OTFjYWNjYTAwMTcxNTc4NDQyYTJhNzRjZWYxNWZmNjRkZGU0NWRmMDdhZmI2NGFlYiIsInJlc3VsdF9pbmRleCI6MCwic3RlcF9jb250cmFjdF9zaGEyNTYiOiIxYTU5NTM2YTQ4OGIxYzQ2YWFjZjBiYTM3OTMwYjg0MDYyYzE0Mjc1NWZhOTM5MTUyZWY1ZTI4YzQwOTQ3MWVkIiwic3RlcF9pbmRleCI6MCwidmVyZGljdCI6InVudmVyaWZpYWJsZSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifV0sInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJxdWFsaWZpY2F0aW9uX2V2aWRlbmNlX29ubHkiOmZhbHNlLCJyZXF1aXJlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjoxLCJpZGVudGl0eSI6MSwicG9zdGNvbmRpdGlvbiI6MX0sInZlcnNpb24iOiJvcGVuYWRhcHQuZXhlY3V0aW9uLW91dGNvbWUvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sImV4ZWN1dGlvbl9vdXRjb21lX3NoYTI1NiI6ImZlMzg5YmFiMGViNDQ0N2NlYzE5OTgyNzRmNzI3Y2E1NzAwNDlmZWZkNTU3ZGI3OTkwNDc2OTlmMmY3OGYxNDIiLCJleGVjdXRpb25fcHVycG9zZSI6InByb2R1Y3Rpb24iLCJmaW5hbF9hdXRob3JpdHlfc2VxdWVuY2UiOjAsImZpbmFsX3J1bnRpbWVfZGVsaXZlcnlfc2VxdWVuY2UiOjksImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowM1oiLCJwZXJtaXRfY2hhaW4iOnsiZW50cmllcyI6W3siZGVsaXZlcnlfcmVjZWlwdF9hcnRpZmFjdCI6eyJwYXlsb2FkIjp7ImF1dGhlbnRpY2F0ZWRfcnVubmVyX2lkX3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJhdXRoZW50aWNhdGVkX3Nlc3Npb25faWRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImRlbGl2ZXJlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDFaIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsIm9uZV91c2VfY2xhaW1faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTAiLCJwZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInBlcm1pdF9pZCI6InBlcm1pdDoxIiwicnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6OSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZWNlaXB0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiI4YThmNmQ4ODc1MGNiYjZiM2JkNjYyMDkzYWQyOWUyYmMyMGM2YWI2OTkxZDcwZTU4ODY5MWM4YjNhYjY2MmI5Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZWNlaXB0LWFydGlmYWN0L3YyIiwic2lnbmF0dXJlIjoiQU9LcUNnUzFzMFhhLWl5RTk2Vi15bFB6UWN2MnV2T3N1eWdIenZ1MlJ5VjdGTHNIRl9MVmZxeXg0dTJwdW1sRWwwWWs1WXU3MEpWVWtzWnBhVXBERHciLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImRlbGl2ZXJ5LWF1dGhvcml0eS1lZDI1NTE5LTI0ZjZlZDZhY2JmZTEwMDkiLCJwdWJsaWNfa2V5IjoiS2F5NjRVRzh5dkN5TGhxVTAwMEx4elllVW0wTC9oTElsNVM4a3lLV2JkYz0ifX0sImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3Rfc2hhMjU2IjoiYzAwYTFiYTcwMTVmM2NiZTA1YTc2YWVhYmZmNjE5ZDc5M2U3Mzc1YTMxMGUxOTVmY2JiZDJkMjk0OGYyYjIxNCIsInBlcm1pdF9hcnRpZmFjdCI6eyJwYXlsb2FkIjp7ImFjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJhZG1pc3Npb25fYXJ0aWZhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImF1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV2aWRlbmNlX2lkZW50aXR5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlucHV0X2VkZ2Vfc2VxdWVuY2UiOjEsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwicGVybWl0X2lkIjoicGVybWl0OjEiLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9jaGVja2VkX2F0IjoiMjAyNi0wOC0xOFQxMTo1OTozMFoiLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9leHBpcmVzX2F0IjoiMjAyNi0wOC0yMFQxMTowMDowMFoiLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9yZXZpc2lvbiI6NywicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1wYXlsb2FkL3YyIn0sInBheWxvYWRfc2hhMjU2IjoiYzg3ZjU3NTFhYTliMjc5Y2NlMjNhYTE3OGFmZjk1NmUxOWY2OTBmY2QyZjlmNzBlMGQ5OTk3ODJhNjkwZDZiOCIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWFydGlmYWN0L3YyIiwic2lnbmF0dXJlIjoiMEJ5SGdxRHhRWnJuZ0xJRi02dFpPcTlsWGFtY0FRQzBkb1VUbmVLTlBlUy1TeW9PYlF5T2tBdVBGSzVQWWNOek9iWnhnc2dBTGRXYUlfc2FVQ3BsQmciLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImRlbGl2ZXJ5LWF1dGhvcml0eS1lZDI1NTE5LTI0ZjZlZDZhY2JmZTEwMDkiLCJwdWJsaWNfa2V5IjoiS2F5NjRVRzh5dkN5TGhxVTAwMEx4elllVW0wTC9oTElsNVM4a3lLV2JkYz0ifX0sInBlcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtY2hhaW4tZW50cnkvdjIifV0sInBlcm1pdF9jaGFpbl9zaGEyNTYiOiI1MDQ4ZmU5NDU4ODJkMjgwMjYwMDhkN2U5MTM3YWQwMTZmZGQwMWFlYWY1ODg0ODFmYjViZDBmMjQyYTUwMDQzIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtY2hhaW4vdjIifSwicGVybWl0X2NvdW50IjoxLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9yZXZpc2lvbiI6NywicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZWNlaXB0Ijp7ImF1dGhvcml6YXRpb25fY29uZmlybWVkIjoxLCJhdXRob3JpemF0aW9uX3JlcXVpcmVkIjoxLCJidW5kbGVfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImR1cmF0aW9uX21zIjoxMDAsImVmZmVjdF90aWVyX3JlYWNoZWQiOiJub25lIiwiZWZmZWN0c19jb25maXJtZWQiOjAsImVmZmVjdHNfcmVxdWlyZWQiOjEsImVzdF9jb3N0X21pY3JvdXNkIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iLCJpZGVudGl0eSJdLCJleHRlcm5hbF9uZXR3b3JrX2NhbGxzIjoibm9uZSIsImdlbmVyYXRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiaGVhbHMiOjAsImlkZW50aXR5X2FwcGxpY2FibGUiOjEsImlkZW50aXR5X2FybWVkIjoxLCJpZGVudGl0eV9jb25maXJtZWQiOjEsImlkZW50aXR5X3JlcXVpcmVkIjoxLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsIm92ZXJfaGFsdF9jb3VudCI6MCwicG9zdGNvbmRpdGlvbnNfY29uZmlybWVkIjowLCJwb3N0Y29uZGl0aW9uc19yZXF1aXJlZCI6MSwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInByb3ZlbmFuY2UiOiJwcm9kdWN0aW9uIiwicmVjZWlwdF9idWlsZGVyX3ZlcnNpb24iOiIxLjIuMyIsInJ1bmdfaGlzdG9ncmFtIjp7fSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1ydW4tcmVjZWlwdC92MSIsInNvdXJjZV9yZWNlaXB0X2RpZ2VzdCI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzb3VyY2VfcmVjZWlwdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3NjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnJ1bi1yZXBvcnQvdjEiLCJzdGVwc19vayI6MCwic3RlcHNfdG90YWwiOjEsInN1YnN0cmF0ZSI6IndlYiIsInRyYW5zYWN0aW9uX291dGNvbWUiOiJSRUNPTkNJTElBVElPTl9SRVFVSVJFRCJ9LCJydW5fcmVjZWlwdF9zaGEyNTYiOiIxOGViMmM0ZjQxNTNkYWIwNTNiZjAyN2MyNmY2OGFmYTA2MTMxN2UzYzJlN2Q1YTYwZWU1OGMzZWU2YjNkN2M2IiwicnVuX3JlcG9ydF9vYmplY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bl9yZXBvcnRfb2JqZWN0X3ZlcnNpb24iOiJ2ZXJzaW9uOnJlY29uY2lsaWF0aW9uOjEiLCJydW5fcmVwb3J0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW50aW1lX3N1YnN0cmF0ZSI6IndlYiIsInJ1bnRpbWVfdmFsaWRhdGlvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfc2VxdWVuY2UiOjEsInZlcmlmaWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwid29ya2Zsb3dfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJ3b3JrZmxvd192ZXJzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA0In0sInNpZ25hdHVyZSI6Imd2ZHZuSlNPUzRWazg3M2k4eDBDMmE5MUE0Qm9WOFFvc05xdmMwaUVHMkJOOXpHOUlwamk5dXI1a0VpME5xdW5HVDdLVFZFczhfLUk2VG1TN21ERUJBIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJldmlkZW5jZS1ydW5uZXItZWQyNTUxOS01NjQ3NWFhNzU0NjM0NzRjIiwicHVibGljX2tleSI6IkE2RUh2L1BPRUw0ZGNOMFk1MHZBbVdmazFqQ2JwUTFmSGR5R1pCSlZNYmc9In19","name":"reconciliation-required-nonempty-permit","payload_canonical_sha256":"ef33d988e8895b431f8d06b134cac38fa0b6628b75c0fbe155e30ebb786bd8bf","signature":"gvdvnJSOS4Vk873i8x0C2a91A4BoV8QosNqvc0iEG2BN9zG9Ipji9ur5kEi0NqunGT7KTVEs8_-I6TmS7mDEBA","terminal_verification_artifact_sha256":"d4959d87ddb589221ddcddc6094bf42948c9357b51769239834815036fc36cad"}]} diff --git a/tests/test_durable_authority_v13.py b/tests/test_durable_authority_v13.py index f484327e..0db17584 100644 --- a/tests/test_durable_authority_v13.py +++ b/tests/test_durable_authority_v13.py @@ -524,6 +524,8 @@ def test_v2_permit_remains_pending_until_signed_receipt_commits_edge( authority.before_initial_delivery(manifest) with pytest.raises(DurableAuthorityBusy, match="remains uncertain"): authority.production_delivery_permit_chain() + with pytest.raises(DurableAuthorityBusy, match="remains uncertain"): + authority.production_delivery_permit_chain(allow_empty=True) entry = authority.acknowledge_remote_delivery(manifest, permit) @@ -535,6 +537,23 @@ def test_v2_permit_remains_pending_until_signed_receipt_commits_edge( assert entry.runtime_delivery_sequence == 0 +def test_pre_actuation_terminal_can_retain_an_empty_delivery_chain( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + manifest, authority = _remote_initial_authority( + tmp_path, + monkeypatch, + _issued_permit_transport("unused", "4" * 64), + ) + + chain = authority.production_delivery_permit_chain(allow_empty=True) + + assert chain.entries == () + assert authority.validate(manifest).delivery_sequence == 0 + with pytest.raises(DurableAuthorityBusy, match="unavailable"): + authority.production_delivery_permit_chain() + + def test_remote_permit_refuses_a_different_hosted_dispatch_session( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/test_hosted_runner_adapter.py b/tests/test_hosted_runner_adapter.py index 1f8b6e08..1debe264 100644 --- a/tests/test_hosted_runner_adapter.py +++ b/tests/test_hosted_runner_adapter.py @@ -58,6 +58,7 @@ ProductionDeliveryPermitChain, ProductionDeliveryPermitPayload, ProductionDeliveryReceiptPayload, + delivery_authority_signer_sha256, evidence_runner_signer_sha256, sign_production_delivery_permit, sign_production_delivery_receipt, @@ -66,7 +67,12 @@ from openadapt_flow.transaction import TransactionOutcome from tests.test_run_receipt import _report as _production_report from tests.test_runner_client_lib import dispatch_payload -from tests.test_terminal_verification_v2 import _payload, _private_key +from tests.test_terminal_verification_v2 import ( + _halted_payload, + _payload, + _private_key, + _reconciliation_payload, +) pytest_plugins = ("tests.test_runner_client_lib",) @@ -182,6 +188,11 @@ def _hosted_dispatch(workflow) -> HostedDispatch: artifact_bytes_base64=b64encode(artifact_raw).decode("ascii"), artifact_sha256=hashlib.sha256(artifact_raw).hexdigest(), ) + authority_key = Ed25519PrivateKey.from_private_bytes(bytes(range(32, 64))) + authority_public_key = authority_key.public_key().public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw, + ) return HostedDispatch( schema_version="openadapt.hosted-runner/v1", dispatch_id="11111111-1111-4111-8111-111111111111", @@ -192,6 +203,11 @@ def _hosted_dispatch(workflow) -> HostedDispatch: run_id=payload.run_id, workflow_id=workflow_id, workflow_version_id=version_id, + execution_authority_id="00000000-0000-4000-8000-000000000008", + execution_authority_sha256="1" * 64, + execution_authority_signer_sha256=delivery_authority_signer_sha256( + authority_public_key + ), idempotency_key="hosted-dispatch-0001", lease_token="oal_" + "a" * 64, lease_expires_at="2099-01-01T00:00:00Z", @@ -618,7 +634,7 @@ class Authority: def __init__(self, _run_dir, _store): pass - def production_delivery_permit_chain(self): + def production_delivery_permit_chain(self, **_kwargs): return chain fixed_now = datetime(2026, 8, 26, 12, 0, 2, tzinfo=timezone.utc) @@ -1211,6 +1227,66 @@ def test_recovery_callback_retains_exact_terminal_v2_envelope(tmp_path, sealed) ) +@pytest.mark.parametrize( + ("outcome", "payload_factory", "uncertain_delivery"), + [ + ( + TransactionOutcome.HALTED_BEFORE_EFFECT, + _halted_payload, + False, + ), + ( + TransactionOutcome.RECONCILIATION_REQUIRED, + _reconciliation_payload, + True, + ), + ], +) +def test_recovery_callback_retains_signed_non_success_terminal_v2( + tmp_path, + sealed, + outcome, + payload_factory, + uncertain_delivery, +) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite") + proof = sign_production_terminal_verification(payload_factory(), _private_key()) + binding = adapter.recovery_binding(dispatch).model_copy( + update={"run_id": proof.payload.run_id} + ) + result = HostedRunResult( + dispatch_id=dispatch.dispatch_id, + run_id=binding.run_id, + outcome=outcome, + evidence_batch=(), + terminal_verification=proof, + started=True, + uncertain_delivery=uncertain_delivery, + report_sha256=proof.payload.run_report_sha256, + ) + + callback = adapter.callback_request(binding, result) + terminal = HostedTerminalEvent.model_validate(callback.events[-1]) + + assert terminal.outcome == outcome.value + assert terminal.uncertain_delivery is uncertain_delivery + assert terminal.terminal_verification_artifact_bytes_base64 is not None + assert terminal.terminal_verification_artifact_sha256 == proof.artifact_sha256() + + +def test_safe_halt_callback_requires_signed_terminal_proof() -> None: + with pytest.raises(ValueError, match="requires exact terminal verification"): + HostedTerminalEvent( + run_id=_halted_payload().run_id, + outcome="HALTED_BEFORE_EFFECT", + report_sha256="b" * 64, + started=True, + uncertain_delivery=False, + ) + + def test_callback_refuses_terminal_proof_for_a_different_run(tmp_path, sealed) -> None: workflow, _ = sealed dispatch = _hosted_dispatch(workflow) diff --git a/tests/test_terminal_verification_v2.py b/tests/test_terminal_verification_v2.py index 66b65fd1..e96245df 100644 --- a/tests/test_terminal_verification_v2.py +++ b/tests/test_terminal_verification_v2.py @@ -12,14 +12,21 @@ from pydantic import ValidationError from openadapt_flow.ir import ( + ActionDeliveryUncertainty, ActionKind, + EffectVerificationEvidence, + ExecutionOutcomeEnvelope, + IdentityCheck, + OutcomeContractCounts, PostconditionContractEvidence, + RunReport, postcondition_contract_sha256, postcondition_step_contract_sha256, ) from openadapt_flow.qualification_admission_v2 import canonical_json from openadapt_flow.receipt import RunReceipt from openadapt_flow.terminal_verification_v2 import ( + SIGNATURE_DOMAIN, ProductionAuthorizationEvidenceManifest, ProductionDeliveryPermit, ProductionDeliveryPermitChain, @@ -35,11 +42,15 @@ ProductionPolicyEvidenceManifest, ProductionPostconditionEvidenceManifest, ProductionRunReceipt, + ProductionTerminalEffectState, + ProductionTerminalVerificationContext, + ProductionTerminalVerificationEnvelope, ProductionTerminalVerificationError, ProductionTerminalVerificationExpected, ProductionTerminalVerificationPayload, TerminalContractCounts, build_evidence_manifest, + build_production_terminal_verification, evidence_runner_signer_sha256, project_production_run_receipt, rebuild_production_delivery_permit_chain_from_artifacts, @@ -48,6 +59,7 @@ sign_production_terminal_verification, verify_production_terminal_verification, ) +from tests.test_run_receipt import _report as _production_report SHA_A = "a" * 64 SHA_B = "b" * 64 @@ -137,6 +149,220 @@ def _outcome() -> ProductionExecutionOutcome: ) +def _terminal_postcondition( + verdict: str, +) -> PostconditionContractEvidence: + return _postcondition().model_copy(update={"verdict": verdict}) + + +def _halted_outcome() -> ProductionExecutionOutcome: + return ProductionExecutionOutcome( + outcome="HALTED", + profile="standard", + production_eligible=False, + execution_completed=False, + required_contracts=TerminalContractCounts( + authorization=1, + identity=1, + postcondition=1, + effect=1, + ), + passed_contracts=TerminalContractCounts( + authorization=1, + identity=1, + postcondition=0, + effect=0, + ), + workflow_contract_sha256=SHA_A, + postcondition_evidence=(_terminal_postcondition("unverifiable"),), + evidence_classes=("authorization", "identity"), + model_calls=0, + external_network_calls="none", + ) + + +def _halted_receipt() -> ProductionRunReceipt: + return ProductionRunReceipt( + source_schema_version="openadapt.run-report/v1", + outcome="HALTED", + transaction_outcome="HALTED_BEFORE_EFFECT", + profile="standard", + production_eligible=False, + steps_total=1, + steps_ok=0, + heals=0, + model_calls=0, + est_cost_microusd=0, + duration_ms=100, + rung_histogram={}, + evidence_classes=("authorization", "identity"), + effect_tier_reached="none", + authorization_required=1, + authorization_confirmed=1, + identity_required=1, + identity_confirmed=1, + postconditions_required=1, + postconditions_confirmed=0, + effects_required=1, + effects_confirmed=0, + identity_armed=1, + identity_applicable=1, + over_halt_count=0, + substrate="web", + provenance="production", + receipt_builder_version="1.2.3", + external_network_calls="none", + bundle_digest=SHA_A, + source_receipt_digest=SHA_B, + source_receipt_sha256=SHA_B, + generated_at="2026-08-18T12:00:00Z", + ) + + +def _reconciliation_outcome() -> ProductionExecutionOutcome: + return ProductionExecutionOutcome( + outcome="HALTED", + profile="standard", + production_eligible=False, + execution_completed=False, + required_contracts=TerminalContractCounts( + authorization=1, + identity=1, + postcondition=1, + effect=1, + ), + passed_contracts=TerminalContractCounts( + authorization=1, + identity=1, + postcondition=0, + effect=0, + ), + workflow_contract_sha256=SHA_A, + postcondition_evidence=(_terminal_postcondition("unverifiable"),), + evidence_classes=("authorization", "identity"), + model_calls=0, + external_network_calls="none", + ) + + +def _reconciliation_receipt() -> ProductionRunReceipt: + return _halted_receipt().model_copy( + update={ + "transaction_outcome": "RECONCILIATION_REQUIRED", + "evidence_classes": ("authorization", "identity"), + "identity_confirmed": 1, + } + ) + + +def _actual_non_success_report( + transaction_outcome: str, +) -> RunReport: + base = _production_report() + assert base.outcome_envelope is not None + base_result = base.results[0] + postcondition = base.outcome_envelope.postcondition_evidence[0].model_copy( + update={"verdict": "unverifiable"} + ) + if transaction_outcome == "HALTED_BEFORE_EFFECT": + identity = IdentityCheck( + status="verified", + mode="structured", + coverage=1.0, + ) + result = base_result.model_copy( + update={ + "risk": "irreversible", + "ok": False, + "identity": identity, + "postconditions_ok": None, + "effect_verified": False, + "effect_evidence": [], + "delivery_attempted": False, + "safety_halt": True, + } + ) + passed = OutcomeContractCounts( + authorization=1, + identity=1, + postcondition=0, + effect=0, + ) + evidence_classes = ["authorization", "identity"] + else: + uncertainty = ActionDeliveryUncertainty( + operation="guarded_coordinate_click", + native=False, + observed_at="2026-07-27T15:34:57+00:00", + cause_type="ConnectionResetError", + verification_attempted=True, + postconditions_confirmed=False, + effects_confirmed=False, + resolved_by_contract=False, + ) + effect = EffectVerificationEvidence( + effect_contract_hash=base_result.effect_contract_hashes[0], + substrate="rest", + verifier_identity="sha256:" + SHA_B, + verification_tier=1, + initial_verdict="indeterminate", + final_verdict="indeterminate", + observed_effect="unknown", + ) + result = base_result.model_copy( + update={ + "risk": "irreversible", + "ok": False, + "postconditions_ok": False, + "effect_verified": False, + "effect_evidence": [effect], + "delivery_attempted": True, + "delivery_uncertainty": uncertainty, + "safety_halt": True, + } + ) + passed = OutcomeContractCounts( + authorization=1, + identity=1, + postcondition=0, + effect=0, + ) + evidence_classes = ["authorization", "identity"] + envelope = ExecutionOutcomeEnvelope( + outcome="HALTED", + profile="standard", + production_eligible=False, + execution_completed=False, + required_contracts=OutcomeContractCounts( + authorization=1, + identity=1, + postcondition=1, + effect=1, + ), + passed_contracts=passed, + workflow_contract_sha256=base.workflow_contract_sha256, + postcondition_evidence=[postcondition], + evidence_classes=evidence_classes, + model_calls=0, + external_network_calls="observed", + ) + return RunReport.model_validate( + base.model_dump(mode="json") + | { + "run_id_sha256": hashlib.sha256(IDS["run_id"].encode()).hexdigest(), + "execution_outcome": "HALTED", + "transaction_outcome": transaction_outcome, + "transaction_billable": False, + "transaction_platform_fault": False, + "production_eligible": False, + "execution_completed": False, + "outcome_envelope": envelope.model_dump(mode="json"), + "success": False, + "results": [result.model_dump(mode="json")], + } + ) + + def _receipt() -> ProductionRunReceipt: unsigned = { "schema_version": "openadapt.run-receipt/v2", @@ -319,6 +545,208 @@ def _manifests( ) +def _halted_manifests( + chain: ProductionDeliveryPermitChain, +) -> ProductionEvidenceManifests: + success = _manifests(_permit_chain()) + authorization = build_evidence_manifest( + ProductionAuthorizationEvidenceManifest, + governed_authorization_id_sha256=SHA_A, + admission_id=IDS["admission_id"], + admission_artifact_sha256=SHA_D, + execution_authority_id=IDS["execution_authority_id"], + execution_authority_sha256=SHA_A, + permit_chain_sha256=chain.permit_chain_sha256, + ) + identity = build_evidence_manifest( + ProductionIdentityEvidenceManifest, + identity_contract_sha256=SHA_D, + workflow_contract_sha256=SHA_A, + required=1, + confirmed=1, + results=( + ProductionIdentityResult( + result_index=0, + status="verified", + mode="structured", + signals=(), + ), + ), + ) + postcondition = build_evidence_manifest( + ProductionPostconditionEvidenceManifest, + workflow_contract_sha256=SHA_A, + required=1, + confirmed=0, + records=(_terminal_postcondition("unverifiable"),), + ) + effect = build_evidence_manifest( + ProductionEffectEvidenceManifest, + effect_contract_sha256=SHA_E, + workflow_contract_sha256=SHA_A, + required=1, + confirmed=0, + records=( + ProductionTerminalEffectState( + result_index=0, + effect_contract_hash="sha256:" + SHA_A, + attempt_state="not_actuated", + observed_effect="absent", + effect_verified=False, + verification_performed=False, + verifier_identity=None, + verification_tier=None, + final_verdict=None, + resolved_delivery_uncertainty=False, + absence_basis="not_actuated", + reconciliation_completed=False, + reconciliation_actions=0, + ), + ), + ) + return ProductionEvidenceManifests( + policy=success.policy, + authorization=authorization, + identity=identity, + postcondition=postcondition, + effect=effect, + ) + + +def _reconciliation_manifests( + chain: ProductionDeliveryPermitChain, +) -> ProductionEvidenceManifests: + halted = _halted_manifests(chain) + identity = build_evidence_manifest( + ProductionIdentityEvidenceManifest, + identity_contract_sha256=SHA_D, + workflow_contract_sha256=SHA_A, + required=1, + confirmed=1, + results=( + ProductionIdentityResult( + result_index=0, + status="verified", + mode="structured", + signals=(), + ), + ), + ) + effect = build_evidence_manifest( + ProductionEffectEvidenceManifest, + effect_contract_sha256=SHA_E, + workflow_contract_sha256=SHA_A, + required=1, + confirmed=0, + records=( + ProductionTerminalEffectState( + result_index=0, + effect_contract_hash="sha256:" + SHA_A, + attempt_state="delivery_uncertain", + observed_effect="unknown", + effect_verified=False, + verification_performed=False, + verifier_identity=None, + verification_tier=None, + final_verdict=None, + resolved_delivery_uncertainty=False, + absence_basis="none", + reconciliation_completed=False, + reconciliation_actions=0, + ), + ), + ) + return halted.model_copy(update={"identity": identity, "effect": effect}) + + +def _halted_payload() -> ProductionTerminalVerificationPayload: + receipt = _halted_receipt() + outcome = _halted_outcome() + chain = ProductionDeliveryPermitChain.build(()) + return ProductionTerminalVerificationPayload( + **IDS, + flow_run_id_sha256=hashlib.sha256(IDS["run_id"].encode("utf-8")).hexdigest(), + bundle_artifact_sha256=SHA_B, + bundle_content_digest=SHA_A, + environment_digest=SHA_A, + environment_contract_sha256=SHA_B, + runtime_environment_sha256=SHA_C, + identity_contract_sha256=SHA_D, + effect_contract_sha256=SHA_E, + runtime_substrate="web", + admission_artifact_sha256=SHA_D, + admission_policy_sha256=SHA_A, + evidence_identity_sha256=SHA_E, + admitted_runtime_build_sha256=SHA_C, + evidence_runner_signer_sha256=evidence_runner_signer_sha256(_public_key()), + qualification_signer_registry_sha256=SHA_E, + qualification_signer_registry_revision=7, + execution_authority_sha256=SHA_A, + execution_authority_signer_sha256=SHA_C, + permit_chain=chain, + permit_count=0, + final_authority_sequence=0, + final_runtime_delivery_sequence=0, + workflow_contract_sha256=SHA_A, + execution_outcome=outcome, + execution_outcome_sha256=outcome.artifact_sha256(), + run_receipt=receipt, + run_receipt_sha256=hashlib.sha256( + canonical_json(receipt.model_dump(mode="json")) + ).hexdigest(), + run_report_sha256=SHA_B, + run_report_object_version="version:halted:1", + run_report_object_sha256=SHA_B, + evidence_manifests=_halted_manifests(chain), + verified_at="2026-08-18T12:00:02Z", + issued_at="2026-08-18T12:00:03Z", + ) + + +def _reconciliation_payload() -> ProductionTerminalVerificationPayload: + chain = _permit_chain() + receipt = _reconciliation_receipt() + outcome = _reconciliation_outcome() + return ProductionTerminalVerificationPayload( + **IDS, + flow_run_id_sha256=hashlib.sha256(IDS["run_id"].encode("utf-8")).hexdigest(), + bundle_artifact_sha256=SHA_B, + bundle_content_digest=SHA_A, + environment_digest=SHA_A, + environment_contract_sha256=SHA_B, + runtime_environment_sha256=SHA_C, + identity_contract_sha256=SHA_D, + effect_contract_sha256=SHA_E, + runtime_substrate="web", + admission_artifact_sha256=SHA_D, + admission_policy_sha256=SHA_A, + evidence_identity_sha256=SHA_E, + admitted_runtime_build_sha256=SHA_C, + evidence_runner_signer_sha256=evidence_runner_signer_sha256(_public_key()), + qualification_signer_registry_sha256=SHA_E, + qualification_signer_registry_revision=7, + execution_authority_sha256=SHA_A, + execution_authority_signer_sha256=chain.entries[0].authority_signer_sha256, + permit_chain=chain, + permit_count=1, + final_authority_sequence=0, + final_runtime_delivery_sequence=9, + workflow_contract_sha256=SHA_A, + execution_outcome=outcome, + execution_outcome_sha256=outcome.artifact_sha256(), + run_receipt=receipt, + run_receipt_sha256=hashlib.sha256( + canonical_json(receipt.model_dump(mode="json")) + ).hexdigest(), + run_report_sha256=SHA_B, + run_report_object_version="version:reconciliation:1", + run_report_object_sha256=SHA_B, + evidence_manifests=_reconciliation_manifests(chain), + verified_at="2026-08-18T12:00:02Z", + issued_at="2026-08-18T12:00:03Z", + ) + + def _payload() -> ProductionTerminalVerificationPayload: receipt = _receipt() chain = _permit_chain() @@ -365,6 +793,16 @@ def _payload() -> ProductionTerminalVerificationPayload: def _expected( payload: ProductionTerminalVerificationPayload, ) -> ProductionTerminalVerificationExpected: + if payload.permit_chain.entries: + authenticated_runner_id_sha256 = payload.permit_chain.entries[ + 0 + ].authenticated_runner_id_sha256 + authenticated_session_id_sha256 = payload.permit_chain.entries[ + 0 + ].authenticated_session_id_sha256 + else: + authenticated_runner_id_sha256 = SHA_B + authenticated_session_id_sha256 = SHA_C return ProductionTerminalVerificationExpected( run_id=payload.run_id, flow_run_id_sha256=payload.flow_run_id_sha256, @@ -396,12 +834,8 @@ def _expected( permit_count=payload.permit_count, final_authority_sequence=payload.final_authority_sequence, final_runtime_delivery_sequence=payload.final_runtime_delivery_sequence, - authenticated_runner_id_sha256=( - payload.permit_chain.entries[0].authenticated_runner_id_sha256 - ), - authenticated_session_id_sha256=( - payload.permit_chain.entries[0].authenticated_session_id_sha256 - ), + authenticated_runner_id_sha256=authenticated_runner_id_sha256, + authenticated_session_id_sha256=authenticated_session_id_sha256, acknowledged_one_use_claim_ids=tuple( entry.one_use_claim_id for entry in payload.permit_chain.entries ), @@ -430,6 +864,173 @@ def test_terminal_v2_signs_and_verifies_exact_production_success() -> None: ) +def test_terminal_v2_signs_and_verifies_zero_permit_safe_halt() -> None: + payload = _halted_payload() + envelope = sign_production_terminal_verification(payload, _private_key()) + + digest = verify_production_terminal_verification( + envelope, + expected=_expected(payload), + now=NOW, + ) + + assert digest == envelope.artifact_sha256() + assert payload.execution_outcome.outcome == "HALTED" + assert payload.run_receipt.transaction_outcome == "HALTED_BEFORE_EFFECT" + assert payload.permit_count == 0 + assert payload.permit_chain.entries == () + assert all( + isinstance(record, ProductionTerminalEffectState) + and record.absence_basis in {"not_actuated", "verifier_refuted"} + for record in payload.evidence_manifests.effect.records + ) + + +def test_terminal_v2_refuses_zero_permit_verified_claim() -> None: + data = _halted_payload().model_dump(mode="json") + data["run_receipt"]["transaction_outcome"] = "VERIFIED" + + with pytest.raises(ValidationError): + ProductionTerminalVerificationPayload.model_validate(data) + + +def test_terminal_v2_refuses_safe_halt_without_effect_absence() -> None: + data = _halted_payload().model_dump(mode="json") + effect = data["evidence_manifests"]["effect"] + record = effect["records"][0] + record.update( + { + "attempt_state": "delivery_uncertain", + "observed_effect": "unknown", + "absence_basis": "none", + } + ) + unsigned = {key: value for key, value in effect.items() if key != "manifest_sha256"} + effect["manifest_sha256"] = hashlib.sha256( + b"openadapt-production-effect-evidence-v1\0" + canonical_json(unsigned) + ).hexdigest() + + with pytest.raises(ValidationError, match="effect absence"): + ProductionTerminalVerificationPayload.model_validate(data) + + +def test_terminal_effect_state_requires_exact_verifier_and_verdict_binding() -> None: + record = _halted_payload().evidence_manifests.effect.records[0] + assert isinstance(record, ProductionTerminalEffectState) + data = record.model_dump(mode="json") + + with pytest.raises(ValidationError, match="verified state"): + ProductionTerminalEffectState.model_validate(data | {"effect_verified": True}) + with pytest.raises(ValidationError, match="verifier identity"): + ProductionTerminalEffectState.model_validate( + data + | { + "attempt_state": "delivered", + "verification_performed": True, + "verification_tier": 1, + "final_verdict": "refuted", + "absence_basis": "verifier_refuted", + } + ) + + refuted = ProductionTerminalEffectState.model_validate( + data + | { + "attempt_state": "delivered", + "verification_performed": True, + "verifier_identity": "sha256:" + SHA_B, + "verification_tier": 1, + "final_verdict": "refuted", + "absence_basis": "verifier_refuted", + } + ) + assert refuted.effect_verified is False + + +def test_terminal_v2_signs_and_verifies_reconciliation_proof() -> None: + payload = _reconciliation_payload() + envelope = sign_production_terminal_verification(payload, _private_key()) + + digest = verify_production_terminal_verification( + envelope, + expected=_expected(payload), + now=NOW, + ) + + assert digest == envelope.artifact_sha256() + assert payload.run_receipt.transaction_outcome == "RECONCILIATION_REQUIRED" + assert payload.permit_count == 1 + record = payload.evidence_manifests.effect.records[0] + assert isinstance(record, ProductionTerminalEffectState) + assert record.attempt_state == "delivery_uncertain" + assert record.observed_effect == "unknown" + + +@pytest.mark.parametrize( + "transaction_outcome", + ["HALTED_BEFORE_EFFECT", "RECONCILIATION_REQUIRED"], +) +def test_terminal_v2_builds_non_success_proof_from_exact_run_report( + transaction_outcome: str, +) -> None: + report = _actual_non_success_report(transaction_outcome) + chain = ( + ProductionDeliveryPermitChain.build(()) + if transaction_outcome == "HALTED_BEFORE_EFFECT" + else _permit_chain() + ) + context = ProductionTerminalVerificationContext( + run_id=IDS["run_id"], + tenant_id=IDS["tenant_id"], + workflow_id=IDS["workflow_id"], + workflow_version_id=IDS["workflow_version_id"], + bundle_version_id=IDS["bundle_version_id"], + bundle_artifact_sha256=SHA_B, + environment_digest=SHA_A, + environment_contract_sha256=SHA_B, + runtime_environment_sha256=SHA_C, + identity_contract_sha256=SHA_D, + effect_contract_sha256=SHA_E, + runtime_validation_id=IDS["runtime_validation_id"], + runtime_substrate="web", + admission_id=IDS["admission_id"], + admission_artifact_sha256=SHA_D, + admission_policy_sha256=SHA_A, + evidence_identity_sha256=SHA_E, + admitted_runtime_build_sha256=SHA_C, + evidence_runner_signer_sha256=evidence_runner_signer_sha256(_public_key()), + qualification_signer_registry_sha256=SHA_E, + qualification_signer_registry_revision=7, + execution_authority_id=IDS["execution_authority_id"], + execution_authority_sha256=SHA_A, + execution_authority_signer_sha256=( + chain.entries[0].authority_signer_sha256 if chain.entries else SHA_C + ), + permit_chain=chain, + run_report_object_version="version:terminal-report:1", + verified_at="2026-08-18T12:00:02Z", + issued_at="2026-08-18T12:00:03Z", + ) + + built = build_production_terminal_verification( + report, + context=context, + private_key=_private_key(), + ) + payload = built.envelope.payload + + assert payload.run_receipt.transaction_outcome == transaction_outcome + assert payload.run_report_sha256 == hashlib.sha256(built.report_bytes).hexdigest() + assert ( + verify_production_terminal_verification( + built.envelope, + expected=_expected(payload), + now=NOW, + ) + == built.envelope.artifact_sha256() + ) + + @pytest.mark.parametrize( ("field", "value", "message"), [ @@ -697,6 +1298,53 @@ def test_delivery_cross_language_vector_is_exact() -> None: assert rebuilt == chain +def test_non_success_terminal_cross_language_vectors_are_exact() -> None: + fixture = json.loads( + Path("tests/fixtures/terminal_verification_v2_terminal_vectors.json").read_text( + encoding="utf-8" + ) + ) + assert b64decode(fixture["signature_domain_base64"], validate=True) == ( + SIGNATURE_DOMAIN + ) + key = Ed25519PrivateKey.from_private_bytes( + b64decode(fixture["private_key_base64"], validate=True) + ) + payloads = { + "halted-before-effect-zero-permit": _halted_payload(), + "reconciliation-required-nonempty-permit": _reconciliation_payload(), + } + + for vector in fixture["vectors"]: + raw = b64decode(vector["envelope_canonical_base64"], validate=True) + envelope = ProductionTerminalVerificationEnvelope.model_validate_json(raw) + payload = payloads[vector["name"]] + expected = sign_production_terminal_verification(payload, key) + + assert canonical_json(envelope) == raw + assert envelope == expected + assert ( + hashlib.sha256(payload.canonical_bytes()).hexdigest() + == (vector["payload_canonical_sha256"]) + ) + assert envelope.signature == vector["signature"] + assert ( + envelope.artifact_sha256() + == (vector["terminal_verification_artifact_sha256"]) + ) + assert ( + envelope.payload.evidence_manifests.effect.records[0].model_dump( + mode="json" + ) + == vector["effect_state"] + ) + callback = vector["callback"] + assert callback["run_id"] == payload.run_id + assert callback["outcome"] == payload.run_receipt.transaction_outcome + assert callback["report_sha256"] == payload.run_report_sha256 + assert callback["artifact_bytes_source"] == "envelope_canonical_base64" + + def test_delivery_artifact_rebuild_rejects_noncanonical_stored_bytes() -> None: entry = _permit_chain().entries[0] with pytest.raises( From bfd5e6959393af9497a05c5e1ae0e7da97661e62 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Thu, 27 Aug 2026 14:16:34 -0400 Subject: [PATCH 6/9] fix(hosted): sign unresolved delivery reconciliation --- openadapt_flow/runner/evidence.py | 20 +- openadapt_flow/runner/hosted_adapter.py | 160 ++++--- openadapt_flow/runtime/durable/authority.py | 31 +- openadapt_flow/terminal_verification_v2.py | 422 +++++++++++++++--- public-artifacts.json | 4 + ...nerate_terminal_verification_v2_vectors.py | 102 +++++ ...inal_verification_v2_terminal_vectors.json | 75 +++- tests/test_durable_authority_v13.py | 16 + tests/test_hosted_runner_adapter.py | 133 ++++-- tests/test_run_receipt.py | 4 + tests/test_terminal_verification_v2.py | 120 ++++- 11 files changed, 919 insertions(+), 168 deletions(-) create mode 100644 scripts/generate_terminal_verification_v2_vectors.py diff --git a/openadapt_flow/runner/evidence.py b/openadapt_flow/runner/evidence.py index adf93849..e8ce0867 100644 --- a/openadapt_flow/runner/evidence.py +++ b/openadapt_flow/runner/evidence.py @@ -207,6 +207,7 @@ def report_events( authorization_id: str, consequential_steps: int, effect_covered_consequential_steps: int, + terminal_outcome: str | None = None, start_seq: int = 1, ) -> list[dict[str, Any]]: """The ordered evidence stream for a completed local run. @@ -241,10 +242,27 @@ def report_events( for result in report.results if result.identity is not None and result.identity.status == "verified" ) + if terminal_outcome is None: + status = summary_status(report) + elif terminal_outcome == "VERIFIED": + status = "confirmed" + elif terminal_outcome == "HALTED_BEFORE_EFFECT": + status = "halted-needs-attention" + elif terminal_outcome in { + "RECONCILIATION_REQUIRED", + "FAILED_PLATFORM", + "CANCELED", + "REJECTED_POLICY", + "COMPLETED_UNVERIFIED", + "ROLLED_BACK", + }: + status = "failed" + else: + raise ValueError("terminal outcome cannot classify the evidence summary") body = _summary_body( bundle_digest=bundle_digest, authorization_id=authorization_id, - status=summary_status(report), + status=status, steps_total=len(report.results), consequential_steps=consequential_steps, effect_covered_consequential_steps=effect_covered_consequential_steps, diff --git a/openadapt_flow/runner/hosted_adapter.py b/openadapt_flow/runner/hosted_adapter.py index 7fd50a0c..7fa80f08 100644 --- a/openadapt_flow/runner/hosted_adapter.py +++ b/openadapt_flow/runner/hosted_adapter.py @@ -313,8 +313,18 @@ def _terminal_outcome_requires_exact_proof(self) -> "HostedTerminalEvent": has_proof = self.terminal_verification_artifact_bytes_base64 is not None if has_proof != (self.terminal_verification_artifact_sha256 is not None): raise ValueError("terminal verification binding is incomplete") - if self.outcome in {"VERIFIED", "HALTED_BEFORE_EFFECT"} and not has_proof: + if ( + self.outcome + in { + "VERIFIED", + "HALTED_BEFORE_EFFECT", + "RECONCILIATION_REQUIRED", + } + and not has_proof + ): raise ValueError(f"{self.outcome} requires exact terminal verification") + if self.uncertain_delivery != (self.outcome == "RECONCILIATION_REQUIRED"): + raise ValueError("terminal uncertainty conflicts with its outcome") if ( self.outcome not in { @@ -373,6 +383,7 @@ def _closed_terminal(self) -> "HostedRunResult": in { TransactionOutcome.VERIFIED, TransactionOutcome.HALTED_BEFORE_EFFECT, + TransactionOutcome.RECONCILIATION_REQUIRED, } and self.terminal_verification is None ): @@ -427,6 +438,34 @@ class CallbackRequest(_Closed): workflow_admission_sha256: str = Field(pattern=_HEX64) events: tuple[dict[str, Any], ...] = Field(min_length=1, max_length=10_001) + @model_validator(mode="after") + def _atomic_terminal_batch(self) -> "CallbackRequest": + terminal_indices = tuple( + index + for index, event in enumerate(self.events) + if event.get("schema_version") == "openadapt.hosted-runner-terminal/v1" + ) + if terminal_indices != (len(self.events) - 1,): + raise ValueError("callback must end in exactly one terminal event") + terminal = HostedTerminalEvent.model_validate(self.events[-1]) + summaries = tuple( + event for event in self.events[:-1] if event.get("kind") == "run_summary" + ) + if len(summaries) != 1: + raise ValueError("callback must contain exactly one run summary") + if any(event.get("run_id") != terminal.run_id for event in self.events[:-1]): + raise ValueError("callback evidence names a different run") + summary = summaries[0].get("run_summary") + if not isinstance(summary, dict): + raise ValueError("callback run summary is invalid") + expected_status = { + "VERIFIED": "confirmed", + "HALTED_BEFORE_EFFECT": "halted-needs-attention", + }.get(terminal.outcome, "failed") + if summary.get("status") != expected_status: + raise ValueError("callback run summary conflicts with terminal outcome") + return self + class CallbackResponse(_Closed): schema_version: Literal["openadapt.hosted-runner-callback-result/v1"] @@ -1199,12 +1238,24 @@ def _produce_terminal_verification( raise ValueError("run report differs from retained governed inputs") prepared = prepare_production_terminal_evidence(report) + now = datetime.now(timezone.utc).replace(microsecond=0) + now_text = now.isoformat().replace("+00:00", "Z") chain = DurableAuthority(run_dir, store).production_delivery_permit_chain( allow_empty=( prepared.transaction_outcome is TransactionOutcome.HALTED_BEFORE_EFFECT - ) + ), + allow_pending=( + prepared.transaction_outcome + is TransactionOutcome.RECONCILIATION_REQUIRED + ), + receipt_absence_observed_at=( + now_text + if prepared.transaction_outcome + is TransactionOutcome.RECONCILIATION_REQUIRED + else None + ), ) - first = chain.entries[0] if chain.entries else None + first = chain.entries[0] if chain.entries else chain.pending expected = qualification.expected runtime = expected.runtime_build_identity flow_run_id_sha256 = hashlib.sha256(dispatch.run_id.encode("utf-8")).hexdigest() @@ -1230,13 +1281,14 @@ def _produce_terminal_verification( != qualification.qualification_signer_registry_sha256 or first.qualification_signer_registry_revision != qualification.qualification_signer_registry.revision - or first.authenticated_runner_id_sha256 != runner_id_sha256 - or first.authenticated_session_id_sha256 != runner_session_id_sha256 ): raise ValueError("retained delivery chain differs from admitted live state") - - now = datetime.now(timezone.utc).replace(microsecond=0) - now_text = now.isoformat().replace("+00:00", "Z") + if chain.entries and ( + chain.entries[0].authenticated_runner_id_sha256 != runner_id_sha256 + or chain.entries[0].authenticated_session_id_sha256 + != runner_session_id_sha256 + ): + raise ValueError("retained delivery chain differs from admitted live state") context = ProductionTerminalVerificationContext( run_id=dispatch.run_id, tenant_id=dispatch.tenant_id, @@ -1286,6 +1338,7 @@ def _produce_terminal_verification( envelope_bytes = canonical_json(built.envelope) payload = built.envelope.payload final = chain.entries[-1] if chain.entries else None + final_authority = chain.pending or final live_expected = ProductionTerminalVerificationExpected( run_id=dispatch.run_id, flow_run_id_sha256=flow_run_id_sha256, @@ -1320,9 +1373,16 @@ def _produce_terminal_verification( dispatch.execution_authority_signer_sha256 ), permit_chain_sha256=chain.permit_chain_sha256, - permit_count=len(chain.entries), + permit_count=len(chain.entries) + (1 if chain.pending is not None else 0), + acknowledged_permit_count=len(chain.entries), + pending_permit_count=(1 if chain.pending is not None else 0), + pending_permit_artifact_sha256=( + chain.pending.permit_artifact_sha256 + if chain.pending is not None + else None + ), final_authority_sequence=( - final.authority_sequence if final is not None else 0 + final_authority.authority_sequence if final_authority is not None else 0 ), final_runtime_delivery_sequence=( final.runtime_delivery_sequence if final is not None else 0 @@ -1445,9 +1505,9 @@ def reconciliation_required( *, code: str = "runner_result_lost", ) -> HostedRunResult: - """Close a crash window without re-entering the execution path.""" + """Refuse an unsigned callback without re-entering the execution path.""" - parsed = HostedRecoveryBinding.model_validate(binding) + HostedRecoveryBinding.model_validate(binding) if ( not code or len(code) > 64 @@ -1457,21 +1517,7 @@ def reconciliation_required( ) ): raise ValueError("reconciliation code is invalid") - return HostedRunResult( - dispatch_id=parsed.dispatch_id, - run_id=parsed.run_id, - outcome=TransactionOutcome.RECONCILIATION_REQUIRED, - evidence_batch=tuple( - failure_events( - run_id=parsed.run_id, - bundle_digest=parsed.bundle_content_digest, - authorization_id=parsed.authorization_id, - ) - ), - started=True, - uncertain_delivery=True, - report_sha256="0" * 64, - ) + raise RuntimeError("managed run requires signed terminal reconciliation") def execute( self, @@ -1622,45 +1668,20 @@ def execute( # Once this call starts, the child can reach a real input edge. Any # lost or malformed result is uncertain and can never be retried. execution = self._runner(argv, run_dir, child_env) - except Exception: + except Exception as exc: outcome = TransactionOutcome.RECONCILIATION_REQUIRED self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) - return HostedRunResult( - dispatch_id=parsed.dispatch_id, - run_id=parsed.run_id, - outcome=outcome, - evidence_batch=tuple( - failure_events( - run_id=parsed.run_id, - bundle_digest=parsed.payload.bundle.content_digest, - authorization_id=parsed.payload.authorization.authorization_id, - ) - ), - started=True, - uncertain_delivery=True, - report_sha256="0" * 64, - ) + raise RuntimeError( + "managed run requires signed terminal reconciliation" + ) from exc if execution.report_bytes is None: outcome = TransactionOutcome.RECONCILIATION_REQUIRED self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) - return HostedRunResult( - dispatch_id=parsed.dispatch_id, - run_id=parsed.run_id, - outcome=outcome, - evidence_batch=tuple( - failure_events( - run_id=parsed.run_id, - bundle_digest=parsed.payload.bundle.content_digest, - authorization_id=parsed.payload.authorization.authorization_id, - ) - ), - started=True, - uncertain_delivery=True, - report_sha256="0" * 64, - ) + raise RuntimeError("managed run requires signed terminal reconciliation") report: RunReport | None = None report_digest = hashlib.sha256(execution.report_bytes).hexdigest() + terminalization_error: Exception | None try: report = RunReport.model_validate_json(execution.report_bytes) outcome = classify_transaction_outcome(report) @@ -1702,19 +1723,23 @@ def execute( ) if proof.payload.run_receipt.transaction_outcome != outcome.value: raise ValueError("terminal proof outcome differs from the report") - except Exception: # noqa: BLE001 - post-delivery terminalization fails closed + except Exception as exc: # noqa: BLE001 - terminalization fails closed outcome = TransactionOutcome.RECONCILIATION_REQUIRED proof = None - if ( - outcome - in { - TransactionOutcome.VERIFIED, - TransactionOutcome.HALTED_BEFORE_EFFECT, - } - and proof is None - ): + terminalization_error = exc + else: + terminalization_error = None + if proof is None and outcome in { + TransactionOutcome.VERIFIED, + TransactionOutcome.HALTED_BEFORE_EFFECT, + TransactionOutcome.RECONCILIATION_REQUIRED, + }: outcome = TransactionOutcome.RECONCILIATION_REQUIRED self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) + if outcome is TransactionOutcome.RECONCILIATION_REQUIRED and proof is None: + raise RuntimeError( + "managed run requires signed terminal reconciliation" + ) from terminalization_error if report is None: events = tuple( failure_events( @@ -1735,6 +1760,7 @@ def execute( effect_covered_consequential_steps=( verified.effect_covered_consequential_steps ), + terminal_outcome=outcome.value, ) ) uncertain = outcome is TransactionOutcome.RECONCILIATION_REQUIRED diff --git a/openadapt_flow/runtime/durable/authority.py b/openadapt_flow/runtime/durable/authority.py index 08113081..03e3fe64 100644 --- a/openadapt_flow/runtime/durable/authority.py +++ b/openadapt_flow/runtime/durable/authority.py @@ -48,6 +48,7 @@ ProductionDeliveryPermitArtifact, ProductionDeliveryPermitChain, ProductionDeliveryReceiptArtifact, + ProductionPendingDeliveryPermit, ) AUTHORITY_DB_ENV = "OPENADAPT_DURABLE_AUTHORITY_DB" @@ -2419,12 +2420,17 @@ def acknowledge_remote_delivery( return entry def production_delivery_permit_chain( - self, *, allow_empty: bool = False + self, + *, + allow_empty: bool = False, + allow_pending: bool = False, + receipt_absence_observed_at: str | None = None, ) -> ProductionDeliveryPermitChain: - """Rebuild the exact acknowledged chain from protected retained bytes.""" + """Rebuild the exact retained chain without resolving uncertain delivery.""" with self._transaction() as connection: - if self._pending_remote_delivery(connection) is not None: + pending = self._pending_remote_delivery(connection) + if pending is not None and not allow_pending: raise DurableAuthorityBusy( "production delivery remains uncertain without a receipt" ) @@ -2434,7 +2440,11 @@ def production_delivery_permit_chain( "ORDER BY runtime_delivery_sequence", (self.path_key,), ).fetchall() - if not rows: + if pending is not None and receipt_absence_observed_at is None: + raise DurableAuthorityBusy( + "pending production delivery requires an exact absence observation" + ) + if not rows and pending is None: if allow_empty: return ProductionDeliveryPermitChain.build(()) raise DurableAuthorityBusy( @@ -2450,7 +2460,18 @@ def production_delivery_permit_chain( for row in rows ) try: - return ProductionDeliveryPermitChain.build(entries) + pending_entry = ( + ProductionPendingDeliveryPermit.build( + pending.permit_artifact, + receipt_absence_observed_at=receipt_absence_observed_at, + ) + if pending is not None and receipt_absence_observed_at is not None + else None + ) + return ProductionDeliveryPermitChain.build( + entries, + pending=pending_entry, + ) except ValueError as exc: raise DurableAuthorityBusy( "the retained production delivery permit chain is invalid" diff --git a/openadapt_flow/terminal_verification_v2.py b/openadapt_flow/terminal_verification_v2.py index 93a13750..ec6d86c2 100644 --- a/openadapt_flow/terminal_verification_v2.py +++ b/openadapt_flow/terminal_verification_v2.py @@ -87,6 +87,9 @@ PERMIT_CHAIN_ENTRY_SCHEMA: Final[ Literal["openadapt.production-delivery-permit-chain-entry/v2"] ] = "openadapt.production-delivery-permit-chain-entry/v2" +PENDING_PERMIT_SCHEMA: Final[ + Literal["openadapt.production-delivery-pending-permit/v2"] +] = "openadapt.production-delivery-pending-permit/v2" EXECUTION_AUTHORITY_DOMAIN: Final[bytes] = ( b"openadapt.production-execution-authority.v2\0" ) @@ -1540,11 +1543,135 @@ def one_use_claim_id(self) -> str: return self._receipt.one_use_claim_id +class ProductionPendingDeliveryPermit(ClosedSignedModel): + """One final signed permit whose delivery receipt is unresolved.""" + + schema_version: Literal["openadapt.production-delivery-pending-permit/v2"] = ( + PENDING_PERMIT_SCHEMA + ) + permit_artifact: ProductionDeliveryPermitArtifact + permit_artifact_sha256: str = Field(pattern=_SHA256_RE) + delivery_state: Literal["UNRESOLVED"] = "UNRESOLVED" + delivery_receipt_artifact: Literal[None] = None + delivery_receipt_artifact_sha256: Literal[None] = None + receipt_absence_observed_at: str + actuation_replay_authorized: Literal[False] = False + + @model_validator(mode="after") + def _closed_pending_edge(self) -> "ProductionPendingDeliveryPermit": + permit = ProductionDeliveryPermitArtifact.model_validate( + self.permit_artifact.model_dump(mode="json") + ) + if self.permit_artifact_sha256 != permit.artifact_sha256(): + raise ValueError("pending delivery permit artifact digest is invalid") + issued = _parse_utc(permit.payload.issued_at, field="permit issued_at") + absence = _parse_utc( + self.receipt_absence_observed_at, + field="pending receipt absence observed_at", + ) + registry_expires = _parse_utc( + permit.payload.qualification_signer_registry_expires_at, + field="permit registry expires_at", + ) + if not issued <= absence < registry_expires: + raise ValueError("pending delivery permit chronology is invalid") + return self + + @classmethod + def build( + cls, + permit_artifact: ProductionDeliveryPermitArtifact, + *, + receipt_absence_observed_at: str, + ) -> "ProductionPendingDeliveryPermit": + return cls( + permit_artifact=permit_artifact, + permit_artifact_sha256=permit_artifact.artifact_sha256(), + receipt_absence_observed_at=receipt_absence_observed_at, + ) + + @property + def _permit(self) -> ProductionDeliveryPermitPayload: + return self.permit_artifact.payload + + @property + def execution_authority_id(self) -> str: + return self._permit.execution_authority_id + + @property + def execution_authority_sha256(self) -> str: + return self._permit.execution_authority_sha256 + + @property + def permit_id(self) -> str: + return self._permit.permit_id + + @property + def run_id(self) -> str: + return self._permit.run_id + + @property + def flow_run_id_sha256(self) -> str: + return self._permit.flow_run_id_sha256 + + @property + def run_request_sha256(self) -> str: + return self._permit.run_request_sha256 + + @property + def action_request_sha256(self) -> str: + return self._permit.action_request_sha256 + + @property + def admission_artifact_sha256(self) -> str: + return self._permit.admission_artifact_sha256 + + @property + def evidence_identity_sha256(self) -> str: + return self._permit.evidence_identity_sha256 + + @property + def environment_digest(self) -> str: + return self._permit.environment_digest + + @property + def qualification_signer_registry_sha256(self) -> str: + return self._permit.qualification_signer_registry_sha256 + + @property + def qualification_signer_registry_revision(self) -> int: + return self._permit.qualification_signer_registry_revision + + @property + def qualification_signer_registry_expires_at(self) -> str: + return self._permit.qualification_signer_registry_expires_at + + @property + def input_edge_sequence(self) -> int: + return self._permit.input_edge_sequence + + @property + def authority_sequence(self) -> int: + return self._permit.authority_sequence + + @property + def issued_at(self) -> str: + return self._permit.issued_at + + @property + def authority_signer_sha256(self) -> str: + return self.permit_artifact.signer.signer_sha256() + + class ProductionDeliveryPermitChain(ClosedSignedModel): schema_version: Literal["openadapt.production-delivery-permit-chain/v2"] = ( PERMIT_CHAIN_SCHEMA ) entries: tuple[ProductionDeliveryPermit, ...] = Field(max_length=10_000) + pending: ProductionPendingDeliveryPermit | None = Field( + default=None, + exclude_if=lambda value: value is None, + ) permit_chain_sha256: str = Field(pattern=_SHA256_RE) @model_validator(mode="after") @@ -1554,35 +1681,39 @@ def _closed_chain(self) -> "ProductionDeliveryPermitChain": # in-memory mutation cannot enter a trusted chain with a stale digest. for item in self.entries: ProductionDeliveryPermit.model_validate(item.model_dump(mode="json")) - expected = hashlib.sha256( - PERMIT_CHAIN_DOMAIN - + canonical_json( - { - "schema_version": self.schema_version, - "entries": [item.model_dump(mode="json") for item in self.entries], - } + if self.pending is not None: + ProductionPendingDeliveryPermit.model_validate( + self.pending.model_dump(mode="json") ) + digest_payload: dict[str, Any] = { + "schema_version": self.schema_version, + "entries": [item.model_dump(mode="json") for item in self.entries], + } + if self.pending is not None: + digest_payload["pending"] = self.pending.model_dump(mode="json") + expected = hashlib.sha256( + PERMIT_CHAIN_DOMAIN + canonical_json(digest_payload) ).hexdigest() if self.permit_chain_sha256 != expected: raise ValueError("delivery permit chain digest is invalid") - if not self.entries: + all_permits: tuple[ + ProductionDeliveryPermit | ProductionPendingDeliveryPermit, ... + ] = (*self.entries, *((self.pending,) if self.pending else ())) + if not all_permits: return self - authority = self.entries[0].execution_authority_id - authority_digest = self.entries[0].execution_authority_sha256 - admission_digest = self.entries[0].admission_artifact_sha256 - evidence_identity_digest = self.entries[0].evidence_identity_sha256 - environment_digest = self.entries[0].environment_digest - registry_digest = self.entries[0].qualification_signer_registry_sha256 - registry_revision = self.entries[0].qualification_signer_registry_revision - registry_expiry = self.entries[0].qualification_signer_registry_expires_at - run_request_digest = self.entries[0].run_request_sha256 - run_id = self.entries[0].run_id - flow_run_id_sha256 = self.entries[0].flow_run_id_sha256 - authority_signer_sha256 = self.entries[0].authority_signer_sha256 - authenticated_runner_id_sha256 = self.entries[0].authenticated_runner_id_sha256 - authenticated_session_id_sha256 = self.entries[ - 0 - ].authenticated_session_id_sha256 + first = all_permits[0] + authority = first.execution_authority_id + authority_digest = first.execution_authority_sha256 + admission_digest = first.admission_artifact_sha256 + evidence_identity_digest = first.evidence_identity_sha256 + environment_digest = first.environment_digest + registry_digest = first.qualification_signer_registry_sha256 + registry_revision = first.qualification_signer_registry_revision + registry_expiry = first.qualification_signer_registry_expires_at + run_request_digest = first.run_request_sha256 + run_id = first.run_id + flow_run_id_sha256 = first.flow_run_id_sha256 + authority_signer_sha256 = first.authority_signer_sha256 if any( item.execution_authority_id != authority or item.execution_authority_sha256 != authority_digest @@ -1596,33 +1727,49 @@ def _closed_chain(self) -> "ProductionDeliveryPermitChain": or item.qualification_signer_registry_expires_at != registry_expiry or item.run_request_sha256 != run_request_digest or item.authority_signer_sha256 != authority_signer_sha256 - or item.authenticated_runner_id_sha256 != authenticated_runner_id_sha256 - or item.authenticated_session_id_sha256 != authenticated_session_id_sha256 - for item in self.entries + for item in all_permits ): raise ValueError("delivery permit chain changes its production authority") - input_sequences = tuple(item.input_edge_sequence for item in self.entries) - authority_sequences = tuple(item.authority_sequence for item in self.entries) + if self.entries: + authenticated_runner_id_sha256 = self.entries[ + 0 + ].authenticated_runner_id_sha256 + authenticated_session_id_sha256 = self.entries[ + 0 + ].authenticated_session_id_sha256 + if any( + item.authenticated_runner_id_sha256 != authenticated_runner_id_sha256 + or item.authenticated_session_id_sha256 + != authenticated_session_id_sha256 + for item in self.entries + ): + raise ValueError( + "delivery permit chain changes its authenticated runner" + ) + input_sequences = tuple(item.input_edge_sequence for item in all_permits) + authority_sequences = tuple(item.authority_sequence for item in all_permits) delivery_sequences = tuple( item.runtime_delivery_sequence for item in self.entries ) - exact = tuple(range(1, len(self.entries) + 1)) + exact = tuple(range(1, len(all_permits) + 1)) if input_sequences != exact: raise ValueError( "delivery input-edge sequence must be exact and contiguous" ) if authority_sequences != tuple( - range(authority_sequences[0], authority_sequences[0] + len(self.entries)) + range(authority_sequences[0], authority_sequences[0] + len(all_permits)) ): raise ValueError("delivery authority sequence must be contiguous") - if delivery_sequences != tuple( - range(delivery_sequences[0], delivery_sequences[0] + len(self.entries)) + if delivery_sequences and delivery_sequences != tuple( + range( + delivery_sequences[0], delivery_sequences[0] + len(delivery_sequences) + ) ): raise ValueError("runtime delivery sequence must be contiguous") - permit_ids = tuple(item.permit_id for item in self.entries) + permit_ids = tuple(item.permit_id for item in all_permits) if len(permit_ids) != len(set(permit_ids)): raise ValueError("delivery permit chain repeats a permit") - action_requests = tuple(item.action_request_sha256 for item in self.entries) + action_requests = tuple(item.action_request_sha256 for item in all_permits) if len(action_requests) != len(set(action_requests)): raise ValueError("delivery permit chain repeats an action request") claim_ids = tuple(item.one_use_claim_id for item in self.entries) @@ -1640,20 +1787,36 @@ def _closed_chain(self) -> "ProductionDeliveryPermitChain": for previous, current in zip(event_times, event_times[1:]) ): raise ValueError("delivery permit chronology is invalid") + if self.pending is not None and self.entries: + final_delivered = _parse_utc( + self.entries[-1].delivered_at, + field="permit delivered_at", + ) + pending_issued = _parse_utc( + self.pending.issued_at, + field="pending permit issued_at", + ) + if final_delivered > pending_issued: + raise ValueError("pending delivery permit chronology is invalid") return self @classmethod def build( - cls, entries: tuple[ProductionDeliveryPermit, ...] + cls, + entries: tuple[ProductionDeliveryPermit, ...], + *, + pending: ProductionPendingDeliveryPermit | None = None, ) -> "ProductionDeliveryPermitChain": - payload = { + payload: dict[str, Any] = { "schema_version": PERMIT_CHAIN_SCHEMA, "entries": [item.model_dump(mode="json") for item in entries], } + if pending is not None: + payload["pending"] = pending.model_dump(mode="json") digest = hashlib.sha256( PERMIT_CHAIN_DOMAIN + canonical_json(payload) ).hexdigest() - return cls(entries=entries, permit_chain_sha256=digest) + return cls(entries=entries, pending=pending, permit_chain_sha256=digest) def build_production_evidence_manifests( @@ -1915,6 +2078,8 @@ class ProductionTerminalVerificationPayload(ClosedSignedModel): execution_authority_signer_sha256: str = Field(pattern=_SHA256_RE) permit_chain: ProductionDeliveryPermitChain permit_count: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + acknowledged_permit_count: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + pending_permit_count: StrictInt = Field(ge=0, le=1) final_authority_sequence: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) final_runtime_delivery_sequence: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) workflow_contract_sha256: str = Field(pattern=_SHA256_RE) @@ -1943,8 +2108,8 @@ def _closed_terminal_outcome(self) -> "ProductionTerminalVerificationPayload": receipt = self.run_receipt outcome = self.execution_outcome manifests = self.evidence_manifests - if chain.entries: - final = chain.entries[-1] + final = chain.pending or (chain.entries[-1] if chain.entries else None) + if final is not None: if ( self.execution_authority_id, self.execution_authority_sha256, @@ -1978,19 +2143,28 @@ def _closed_terminal_outcome(self) -> "ProductionTerminalVerificationPayload": raise ValueError( "terminal qualification state does not match its permits" ) + expected_final_delivery_sequence = ( + chain.entries[-1].runtime_delivery_sequence if chain.entries else 0 + ) if ( self.permit_count, + self.acknowledged_permit_count, + self.pending_permit_count, self.final_authority_sequence, self.final_runtime_delivery_sequence, ) != ( + len(chain.entries) + (1 if chain.pending is not None else 0), len(chain.entries), + 1 if chain.pending is not None else 0, final.authority_sequence, - final.runtime_delivery_sequence, + expected_final_delivery_sequence, ): raise ValueError("terminal permit counts do not match the permit chain") elif ( receipt.transaction_outcome != "HALTED_BEFORE_EFFECT" or self.permit_count != 0 + or self.acknowledged_permit_count != 0 + or self.pending_permit_count != 0 or self.final_authority_sequence != 0 or self.final_runtime_delivery_sequence != 0 ): @@ -2074,7 +2248,8 @@ def _closed_terminal_outcome(self) -> "ProductionTerminalVerificationPayload": outcome.outcome != "VERIFIED" or not outcome.production_eligible or not outcome.execution_completed - or not chain.entries + or self.acknowledged_permit_count < 1 + or self.pending_permit_count != 0 ): raise ValueError("terminal VERIFIED proof is incomplete") elif transaction_outcome == "HALTED_BEFORE_EFFECT": @@ -2083,6 +2258,7 @@ def _closed_terminal_outcome(self) -> "ProductionTerminalVerificationPayload": or outcome.production_eligible or outcome.execution_completed or receipt.production_eligible + or self.permit_count != 0 or any( not isinstance(item, ProductionTerminalEffectState) or item.absence_basis not in {"not_actuated", "verifier_refuted"} @@ -2090,13 +2266,46 @@ def _closed_terminal_outcome(self) -> "ProductionTerminalVerificationPayload": ) ): raise ValueError("terminal HALTED proof lacks exact effect absence") - elif ( - transaction_outcome != "RECONCILIATION_REQUIRED" - or outcome.outcome == "VERIFIED" - or outcome.production_eligible - or receipt.production_eligible - or not chain.entries - ): + elif transaction_outcome == "RECONCILIATION_REQUIRED": + if ( + outcome.outcome == "VERIFIED" + or outcome.production_eligible + or receipt.production_eligible + or self.permit_count < 1 + ): + raise ValueError("terminal reconciliation proof is invalid") + if chain.pending is not None: + uncertain_records = tuple( + item + for item in manifests.effect.records + if isinstance(item, ProductionTerminalEffectState) + and item.attempt_state == "delivery_uncertain" + ) + if not uncertain_records or any( + item.observed_effect not in {"conflicting", "unknown"} + or item.resolved_delivery_uncertainty + or item.absence_basis != "none" + for item in uncertain_records + ): + raise ValueError( + "terminal pending delivery lacks unresolved effect evidence" + ) + elif not any( + isinstance(item, ProductionTerminalEffectState) + and ( + item.final_verdict == "indeterminate" + or item.observed_effect in {"conflicting", "unknown"} + or ( + item.attempt_state == "delivery_uncertain" + and not item.resolved_delivery_uncertainty + ) + ) + for item in manifests.effect.records + ): + raise ValueError( + "terminal reconciliation proof lacks inconclusive effect evidence" + ) + else: raise ValueError("terminal reconciliation proof is invalid") if transaction_outcome != "VERIFIED" and ( receipt.source_receipt_digest != self.run_report_sha256 @@ -2107,7 +2316,24 @@ def _closed_terminal_outcome(self) -> "ProductionTerminalVerificationPayload": issued = _parse_utc(self.issued_at, field="terminal issued_at") if not verified <= issued <= verified + timedelta(minutes=5): raise ValueError("terminal proof issue time is invalid") - if chain.entries: + if chain.pending is not None: + pending_issued = _parse_utc( + chain.pending.issued_at, + field="pending permit issued_at", + ) + absence_observed = _parse_utc( + chain.pending.receipt_absence_observed_at, + field="pending receipt absence observed_at", + ) + registry_expires = _parse_utc( + chain.pending.qualification_signer_registry_expires_at, + field="pending permit registry expires_at", + ) + if not pending_issued <= absence_observed <= verified < registry_expires: + raise ValueError( + "terminal verification is outside pending permit chronology" + ) + elif chain.entries: final = chain.entries[-1] final_delivered = _parse_utc( final.delivered_at, field="permit delivered_at" @@ -2220,6 +2446,12 @@ class ProductionTerminalVerificationExpected(ClosedSignedModel): execution_authority_signer_sha256: str = Field(pattern=_SHA256_RE) permit_chain_sha256: str = Field(pattern=_SHA256_RE) permit_count: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + acknowledged_permit_count: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + pending_permit_count: StrictInt = Field(ge=0, le=1) + pending_permit_artifact_sha256: str | None = Field( + default=None, + pattern=_SHA256_RE, + ) final_authority_sequence: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) final_runtime_delivery_sequence: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) authenticated_runner_id_sha256: str = Field(pattern=_SHA256_RE) @@ -2235,10 +2467,21 @@ class ProductionTerminalVerificationExpected(ClosedSignedModel): @model_validator(mode="after") def _closed_delivery_state(self) -> "ProductionTerminalVerificationExpected": - if len(self.acknowledged_one_use_claim_ids) != self.permit_count: + if self.permit_count != ( + self.acknowledged_permit_count + self.pending_permit_count + ): + raise ValueError("live delivery permit counts are inconsistent") + if len(self.acknowledged_one_use_claim_ids) != self.acknowledged_permit_count: raise ValueError("live delivery claim count does not match permit count") - if len(set(self.acknowledged_one_use_claim_ids)) != self.permit_count: + if ( + len(set(self.acknowledged_one_use_claim_ids)) + != self.acknowledged_permit_count + ): raise ValueError("live delivery claims are not globally one-use") + if (self.pending_permit_artifact_sha256 is not None) != ( + self.pending_permit_count == 1 + ): + raise ValueError("live pending delivery binding is inconsistent") for claim_id in self.acknowledged_one_use_claim_ids: if _UUID_RE.fullmatch(claim_id) is None: raise ValueError("live delivery claim id is invalid") @@ -2308,6 +2551,7 @@ class BuiltProductionTerminalVerification: "authenticated_runner_id_sha256", "authenticated_session_id_sha256", "acknowledged_one_use_claim_ids", + "pending_permit_artifact_sha256", } ) @@ -2401,8 +2645,19 @@ def build_production_terminal_verification( execution_authority_sha256=context.execution_authority_sha256, execution_authority_signer_sha256=(context.execution_authority_signer_sha256), permit_chain=context.permit_chain, - permit_count=len(context.permit_chain.entries), - final_authority_sequence=(final.authority_sequence if final is not None else 0), + permit_count=( + len(context.permit_chain.entries) + + (1 if context.permit_chain.pending is not None else 0) + ), + acknowledged_permit_count=len(context.permit_chain.entries), + pending_permit_count=(1 if context.permit_chain.pending is not None else 0), + final_authority_sequence=( + context.permit_chain.pending.authority_sequence + if context.permit_chain.pending is not None + else final.authority_sequence + if final is not None + else 0 + ), final_runtime_delivery_sequence=( final.runtime_delivery_sequence if final is not None else 0 ), @@ -2487,6 +2742,13 @@ def verify_production_terminal_verification( raise ProductionTerminalVerificationError( "production terminal one-use claims do not match live state" ) + pending_permit_artifact_sha256 = ( + chain.pending.permit_artifact_sha256 if chain.pending is not None else None + ) + if pending_permit_artifact_sha256 != expected.pending_permit_artifact_sha256: + raise ProductionTerminalVerificationError( + "production terminal pending permit does not match live state" + ) current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc) if _parse_utc(payload.issued_at, field="terminal issued_at") > current + timedelta( minutes=5 @@ -2499,6 +2761,8 @@ def verify_production_terminal_verification( def rebuild_production_delivery_permit_chain( permits: tuple[ProductionDeliveryPermit, ...], + *, + pending: ProductionPendingDeliveryPermit | None = None, ) -> ProductionDeliveryPermitChain: """Rebuild the permit chain from independently retained permit records. @@ -2508,15 +2772,33 @@ def rebuild_production_delivery_permit_chain( rebuilds the chain, and passes the rebuilt digest as the expected value. """ - return ProductionDeliveryPermitChain.build(permits) + if not permits and pending is None: + raise ProductionTerminalVerificationError( + "retained production delivery artifact count is invalid" + ) + if len(permits) + (1 if pending is not None else 0) > 10_000: + raise ProductionTerminalVerificationError( + "retained production delivery artifact count is invalid" + ) + return ProductionDeliveryPermitChain.build(permits, pending=pending) def rebuild_production_delivery_permit_chain_from_artifacts( artifacts: tuple[tuple[bytes, bytes], ...], + *, + pending_permit_artifact: bytes | None = None, + receipt_absence_observed_at: str | None = None, ) -> ProductionDeliveryPermitChain: """Rebuild a chain from exact retained permit and receipt envelope bytes.""" - if not artifacts or len(artifacts) > 10_000: + has_pending = pending_permit_artifact is not None + if has_pending != (receipt_absence_observed_at is not None): + raise ProductionTerminalVerificationError( + "retained pending delivery binding is incomplete" + ) + if (not artifacts and not has_pending) or len(artifacts) + ( + 1 if has_pending else 0 + ) > 10_000: raise ProductionTerminalVerificationError( "retained production delivery artifact count is invalid" ) @@ -2550,7 +2832,33 @@ def rebuild_production_delivery_permit_chain_from_artifacts( "retained production delivery artifact is not canonical" ) entries.append(ProductionDeliveryPermit.build(permit, receipt)) - return ProductionDeliveryPermitChain.build(tuple(entries)) + pending: ProductionPendingDeliveryPermit | None = None + if pending_permit_artifact is not None: + if ( + len(pending_permit_artifact) == 0 + or len(pending_permit_artifact) > MAX_DELIVERY_ARTIFACT_BYTES + ): + raise ProductionTerminalVerificationError( + "retained pending delivery artifact size is invalid" + ) + try: + permit = ProductionDeliveryPermitArtifact.model_validate_json( + pending_permit_artifact + ) + except ValueError as exc: + raise ProductionTerminalVerificationError( + "retained pending delivery artifact is invalid" + ) from exc + if permit.canonical_bytes() != pending_permit_artifact: + raise ProductionTerminalVerificationError( + "retained pending delivery artifact is not canonical" + ) + assert receipt_absence_observed_at is not None + pending = ProductionPendingDeliveryPermit.build( + permit, + receipt_absence_observed_at=receipt_absence_observed_at, + ) + return ProductionDeliveryPermitChain.build(tuple(entries), pending=pending) def verify_production_terminal_verification_from_report( diff --git a/public-artifacts.json b/public-artifacts.json index ab6c81d8..35bd2386 100644 --- a/public-artifacts.json +++ b/public-artifacts.json @@ -5779,6 +5779,10 @@ "path": "tests/fixtures/terminal_verification_v2_delivery_vector.json", "sha256": "20a1ae7f1878315c100c58894ec14bbc6af4848a6c206c467ca82cff025325f0" }, + { + "path": "tests/fixtures/terminal_verification_v2_terminal_vectors.json", + "sha256": "82d609a0c29593c91ec42b9416a64d6432fad47e3898bbfb926e76f7598f91df" + }, { "path": "tests/fixtures/theme-region-stable-overhalt-v1.16.1.json", "sha256": "4bf055e6c11f837ed6daa4a5b89ab28dd431a7eccd2ce3044758ba260eed52cd" diff --git a/scripts/generate_terminal_verification_v2_vectors.py b/scripts/generate_terminal_verification_v2_vectors.py new file mode 100644 index 00000000..2b7ea7f3 --- /dev/null +++ b/scripts/generate_terminal_verification_v2_vectors.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Regenerate the deterministic non-success terminal-v2 test vectors.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from base64 import b64encode +from pathlib import Path + +from cryptography.hazmat.primitives import serialization + +from openadapt_flow.qualification_admission_v2 import canonical_json +from openadapt_flow.terminal_verification_v2 import ( + SIGNATURE_DOMAIN, + evidence_runner_key_id, + evidence_runner_signer_sha256, + sign_production_terminal_verification, +) +from tests.test_terminal_verification_v2 import ( + _halted_payload, + _private_key, + _reconciliation_payload, +) + +DEFAULT_OUTPUT = Path("tests/fixtures/terminal_verification_v2_terminal_vectors.json") + + +def _vector(name: str, *, uncertain_delivery: bool) -> dict[str, object]: + payload = ( + _halted_payload() + if name == "halted-before-effect-zero-permit" + else _reconciliation_payload() + ) + envelope = sign_production_terminal_verification(payload, _private_key()) + raw = canonical_json(envelope) + effect_state = payload.evidence_manifests.effect.records[0] + return { + "callback": { + "artifact_bytes_source": "envelope_canonical_base64", + "outcome": payload.run_receipt.transaction_outcome, + "report_sha256": payload.run_report_sha256, + "run_id": payload.run_id, + "schema_version": "openadapt.hosted-runner-terminal/v1", + "started": True, + "uncertain_delivery": uncertain_delivery, + }, + "effect_state": effect_state.model_dump(mode="json"), + "envelope_canonical_base64": b64encode(raw).decode("ascii"), + "name": name, + "payload_canonical_sha256": hashlib.sha256( + payload.canonical_bytes() + ).hexdigest(), + "signature": envelope.signature, + "terminal_verification_artifact_sha256": envelope.artifact_sha256(), + } + + +def build_fixture() -> dict[str, object]: + private_key = _private_key() + private_bytes = private_key.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), + ) + public_bytes = private_key.public_key().public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + return { + "key_id": evidence_runner_key_id(public_bytes), + "private_key_base64": b64encode(private_bytes).decode("ascii"), + "public_key_base64": b64encode(public_bytes).decode("ascii"), + "schema_version": "openadapt.production-terminal-cross-language-vectors/v2", + "signature_domain_base64": b64encode(SIGNATURE_DOMAIN).decode("ascii"), + "signer_sha256": evidence_runner_signer_sha256(public_bytes), + "vectors": [ + _vector( + "halted-before-effect-zero-permit", + uncertain_delivery=False, + ), + _vector( + "reconciliation-required-pending-permit", + uncertain_delivery=True, + ), + ], + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + args = parser.parse_args() + args.output.write_text( + json.dumps(build_fixture(), indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/terminal_verification_v2_terminal_vectors.json b/tests/fixtures/terminal_verification_v2_terminal_vectors.json index 5764f76e..5d7ceabc 100644 --- a/tests/fixtures/terminal_verification_v2_terminal_vectors.json +++ b/tests/fixtures/terminal_verification_v2_terminal_vectors.json @@ -1 +1,74 @@ -{"key_id":"evidence-runner-ed25519-56475aa75463474c","private_key_base64":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=","public_key_base64":"A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=","schema_version":"openadapt.production-terminal-cross-language-vectors/v2","signature_domain_base64":"b3BlbmFkYXB0LXByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uLXYyAA==","signer_sha256":"56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c","vectors":[{"callback":{"artifact_bytes_source":"envelope_canonical_base64","outcome":"HALTED_BEFORE_EFFECT","report_sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","run_id":"00000000-0000-4000-8000-000000000001","schema_version":"openadapt.hosted-runner-terminal/v1","started":true,"uncertain_delivery":false},"effect_state":{"absence_basis":"not_actuated","attempt_state":"not_actuated","effect_contract_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","effect_verified":false,"final_verdict":null,"observed_effect":"absent","reconciliation_actions":0,"reconciliation_completed":false,"record_kind":"terminal_effect_state","resolved_delivery_uncertainty":false,"result_index":0,"verification_performed":false,"verification_tier":null,"verifier_identity":null},"envelope_canonical_base64":"eyJwYXlsb2FkIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6IjMyY2QzNjEzYmVlOWJjNjkwNjkzYTM5NWQ5OTMzNTQ3ZjllZWI5ZjZmYWE1ZGJkYmIxNGExMDYyOThiZTQ2NWMiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiODJkYWI1NDI4ZTI1MmQxOGE1OWIwMTc3NzM5ZTAwY2FhZGI3NjJkY2Y1OWY0MDM2ZGVhNGQ3N2MzYzMyMDNjYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6ImViYjU4MzYxNzhlZGZhYzYyOWJkOWJjYTkwZWNkMDE5NTkwOTQ5NjZhMGRkNjNiYWVhN2E2MmY2ZTNiMDRmZjgiLCJyZWNvcmRzIjpbeyJhYnNlbmNlX2Jhc2lzIjoibm90X2FjdHVhdGVkIiwiYXR0ZW1wdF9zdGF0ZSI6Im5vdF9hY3R1YXRlZCIsImVmZmVjdF9jb250cmFjdF9oYXNoIjoic2hhMjU2OmFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJlZmZlY3RfdmVyaWZpZWQiOmZhbHNlLCJmaW5hbF92ZXJkaWN0IjpudWxsLCJvYnNlcnZlZF9lZmZlY3QiOiJhYnNlbnQiLCJyZWNvbmNpbGlhdGlvbl9hY3Rpb25zIjowLCJyZWNvbmNpbGlhdGlvbl9jb21wbGV0ZWQiOmZhbHNlLCJyZWNvcmRfa2luZCI6InRlcm1pbmFsX2VmZmVjdF9zdGF0ZSIsInJlc29sdmVkX2RlbGl2ZXJ5X3VuY2VydGFpbnR5IjpmYWxzZSwicmVzdWx0X2luZGV4IjowLCJ2ZXJpZmljYXRpb25fcGVyZm9ybWVkIjpmYWxzZSwidmVyaWZpY2F0aW9uX3RpZXIiOm51bGwsInZlcmlmaWVyX2lkZW50aXR5IjpudWxsfV0sInJlcXVpcmVkIjoxLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWVmZmVjdC1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiaWRlbnRpdHkiOnsiY29uZmlybWVkIjoxLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiMTM1Yzg2M2VkYzdmYWIxOGNhMjliNDkyOTNmYzI1YTFjOWJkNGRlMGU5ZWQyOGQ0Y2VjNmU4ZDBhNjc5MTIyOSIsInJlcXVpcmVkIjoxLCJyZXN1bHRzIjpbeyJtb2RlIjoic3RydWN0dXJlZCIsInJlc3VsdF9pbmRleCI6MCwic2lnbmFscyI6W10sInN0YXR1cyI6InZlcmlmaWVkIn1dLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWlkZW50aXR5LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJwb2xpY3kiOnsiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX3BvbGljeV9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZ292ZXJuZWRfcnVudGltZV9pbnB1dHNfZGlnZXN0IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiI0Zjk4OGQwMTViY2Q3Y2Q3ZTA1YmEwNjYzZDg5ZjBhOTY4ZjBkOGFjZTk5YjA5MGViYWJlZTUyYTFjNTUwNDIxIiwibWluaW11bV9lZmZlY3RfdGllciI6MSwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb2xpY3ktZXZpZGVuY2UvdjEifSwicG9zdGNvbmRpdGlvbiI6eyJjb25maXJtZWQiOjAsIm1hbmlmZXN0X3NoYTI1NiI6IjNlYTRjNjRhMzBiMGNmMTdiY2EwNGYzMjYwZTBhNDcxOGIyN2Y1MWUxZmRjMmJiODhhMTgyNjMwYTM0ZGQ1ZmYiLCJyZWNvcmRzIjpbeyJhY3Rpb25fa2luZCI6ImNsaWNrIiwiYWN0dWF0aW9uX3BhdGgiOiJndWkiLCJjb250cmFjdF9pbmRleCI6MCwiY29udHJhY3Rfa2luZCI6ImV4cGxpY2l0X3ByZWRpY2F0ZSIsImNvbnRyYWN0X3NoYTI1NiI6IjBhZGUwYjUyMGJjMDA3ODkxY2FjY2EwMDE3MTU3ODQ0MmEyYTc0Y2VmMTVmZjY0ZGRlNDVkZjA3YWZiNjRhZWIiLCJyZXN1bHRfaW5kZXgiOjAsInN0ZXBfY29udHJhY3Rfc2hhMjU2IjoiMWE1OTUzNmE0ODhiMWM0NmFhY2YwYmEzNzkzMGI4NDA2MmMxNDI3NTVmYTkzOTE1MmVmNWUyOGM0MDk0NzFlZCIsInN0ZXBfaW5kZXgiOjAsInZlcmRpY3QiOiJ1bnZlcmlmaWFibGUiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn1dLCJyZXF1aXJlZCI6MSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb3N0Y29uZGl0aW9uLWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9fSwiZXZpZGVuY2VfcnVubmVyX3NpZ25lcl9zaGEyNTYiOiI1NjQ3NWFhNzU0NjM0NzRjMDI4NWRmNWRiZjJiY2FiNzNkYTY1MTM1ODgzOWU5Yjc3NDgxYjJlYWIxMDc3MDhjIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2lnbmVyX3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJleGVjdXRpb25fb3V0Y29tZSI6eyJjb21wZW5zYXRpb25fYWN0aW9ucyI6MCwiZXZpZGVuY2VfY2xhc3NlcyI6WyJhdXRob3JpemF0aW9uIiwiaWRlbnRpdHkiXSwiZXhlY3V0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJub25lIiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJwYXNzZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjEsInBvc3Rjb25kaXRpb24iOjB9LCJwb3N0Y29uZGl0aW9uX2V2aWRlbmNlIjpbeyJhY3Rpb25fa2luZCI6ImNsaWNrIiwiYWN0dWF0aW9uX3BhdGgiOiJndWkiLCJjb250cmFjdF9pbmRleCI6MCwiY29udHJhY3Rfa2luZCI6ImV4cGxpY2l0X3ByZWRpY2F0ZSIsImNvbnRyYWN0X3NoYTI1NiI6IjBhZGUwYjUyMGJjMDA3ODkxY2FjY2EwMDE3MTU3ODQ0MmEyYTc0Y2VmMTVmZjY0ZGRlNDVkZjA3YWZiNjRhZWIiLCJyZXN1bHRfaW5kZXgiOjAsInN0ZXBfY29udHJhY3Rfc2hhMjU2IjoiMWE1OTUzNmE0ODhiMWM0NmFhY2YwYmEzNzkzMGI4NDA2MmMxNDI3NTVmYTkzOTE1MmVmNWUyOGM0MDk0NzFlZCIsInN0ZXBfaW5kZXgiOjAsInZlcmRpY3QiOiJ1bnZlcmlmaWFibGUiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn1dLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjpmYWxzZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicXVhbGlmaWNhdGlvbl9ldmlkZW5jZV9vbmx5IjpmYWxzZSwicmVxdWlyZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MSwiaWRlbnRpdHkiOjEsInBvc3Rjb25kaXRpb24iOjF9LCJ2ZXJzaW9uIjoib3BlbmFkYXB0LmV4ZWN1dGlvbi1vdXRjb21lL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJleGVjdXRpb25fb3V0Y29tZV9zaGEyNTYiOiJmZTM4OWJhYjBlYjQ0NDdjZWMxOTk4Mjc0ZjcyN2NhNTcwMDQ5ZmVmZDU1N2RiNzk5MDQ3Njk5ZjJmNzhmMTQyIiwiZXhlY3V0aW9uX3B1cnBvc2UiOiJwcm9kdWN0aW9uIiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjowLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDNaIiwicGVybWl0X2NoYWluIjp7ImVudHJpZXMiOltdLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiODJkYWI1NDI4ZTI1MmQxOGE1OWIwMTc3NzM5ZTAwY2FhZGI3NjJkY2Y1OWY0MDM2ZGVhNGQ3N2MzYzMyMDNjYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWNoYWluL3YyIn0sInBlcm1pdF9jb3VudCI6MCwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVjZWlwdCI6eyJhdXRob3JpemF0aW9uX2NvbmZpcm1lZCI6MSwiYXV0aG9yaXphdGlvbl9yZXF1aXJlZCI6MSwiYnVuZGxlX2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJkdXJhdGlvbl9tcyI6MTAwLCJlZmZlY3RfdGllcl9yZWFjaGVkIjoibm9uZSIsImVmZmVjdHNfY29uZmlybWVkIjowLCJlZmZlY3RzX3JlcXVpcmVkIjoxLCJlc3RfY29zdF9taWNyb3VzZCI6MCwiZXZpZGVuY2VfY2xhc3NlcyI6WyJhdXRob3JpemF0aW9uIiwiaWRlbnRpdHkiXSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJnZW5lcmF0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImhlYWxzIjowLCJpZGVudGl0eV9hcHBsaWNhYmxlIjoxLCJpZGVudGl0eV9hcm1lZCI6MSwiaWRlbnRpdHlfY29uZmlybWVkIjoxLCJpZGVudGl0eV9yZXF1aXJlZCI6MSwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJvdmVyX2hhbHRfY291bnQiOjAsInBvc3Rjb25kaXRpb25zX2NvbmZpcm1lZCI6MCwicG9zdGNvbmRpdGlvbnNfcmVxdWlyZWQiOjEsInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJwcm92ZW5hbmNlIjoicHJvZHVjdGlvbiIsInJlY2VpcHRfYnVpbGRlcl92ZXJzaW9uIjoiMS4yLjMiLCJydW5nX2hpc3RvZ3JhbSI6e30sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcnVuLXJlY2VpcHQvdjEiLCJzb3VyY2VfcmVjZWlwdF9kaWdlc3QiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3JlY2VpcHRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9zY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5ydW4tcmVwb3J0L3YxIiwic3RlcHNfb2siOjAsInN0ZXBzX3RvdGFsIjoxLCJzdWJzdHJhdGUiOiJ3ZWIiLCJ0cmFuc2FjdGlvbl9vdXRjb21lIjoiSEFMVEVEX0JFRk9SRV9FRkZFQ1QifSwicnVuX3JlY2VpcHRfc2hhMjU2IjoiYWI1MGEwMDZhODg4YjNmOTBlMWU3MWVmMjA5MjFhZjcxN2IyZjQxYTM2NzZlNjA1ZmJkYjgyMGNiNmI1M2NjNCIsInJ1bl9yZXBvcnRfb2JqZWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW5fcmVwb3J0X29iamVjdF92ZXJzaW9uIjoidmVyc2lvbjpoYWx0ZWQ6MSIsInJ1bl9yZXBvcnRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bnRpbWVfc3Vic3RyYXRlIjoid2ViIiwicnVudGltZV92YWxpZGF0aW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA1Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi10ZXJtaW5hbC12ZXJpZmljYXRpb24vdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9zZXF1ZW5jZSI6MSwidmVyaWZpZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJ3b3JrZmxvd19pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMyIsIndvcmtmbG93X3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQifSwic2lnbmF0dXJlIjoiNUptaHdlQXN0OGZ0eUwtbjd4a1JLNTE0cEJCbkU1TzlobUZ3RjRDbmtKOEtpVHBKeExFQWs2ZHNzTUhfTkVSM2QzbVFOeENBQkNHckRkY29tSkE5Q1EiLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImV2aWRlbmNlLXJ1bm5lci1lZDI1NTE5LTU2NDc1YWE3NTQ2MzQ3NGMiLCJwdWJsaWNfa2V5IjoiQTZFSHYvUE9FTDRkY04wWTUwdkFtV2ZrMWpDYnBRMWZIZHlHWkJKVk1iZz0ifX0=","name":"halted-before-effect-zero-permit","payload_canonical_sha256":"26f4073246b4998650636bd3f1cef10835c7f51d91ccfa1d0854bc67015e6792","signature":"5JmhweAst8ftyL-n7xkRK514pBBnE5O9hmFwF4CnkJ8KiTpJxLEAk6dssMH_NER3d3mQNxCABCGrDdcomJA9CQ","terminal_verification_artifact_sha256":"43f6fc7809d789aa42785b7f76ce2dd751b2c3251c8fc74ec1f7bfe3a70b6577"},{"callback":{"artifact_bytes_source":"envelope_canonical_base64","outcome":"RECONCILIATION_REQUIRED","report_sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","run_id":"00000000-0000-4000-8000-000000000001","schema_version":"openadapt.hosted-runner-terminal/v1","started":true,"uncertain_delivery":true},"effect_state":{"absence_basis":"none","attempt_state":"delivery_uncertain","effect_contract_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","effect_verified":false,"final_verdict":null,"observed_effect":"unknown","reconciliation_actions":0,"reconciliation_completed":false,"record_kind":"terminal_effect_state","resolved_delivery_uncertainty":false,"result_index":0,"verification_performed":false,"verification_tier":null,"verifier_identity":null},"envelope_canonical_base64":"eyJwYXlsb2FkIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6ImYwY2ExOWE2YjNlYWJlNTk0ZGJjOTA2ODljMmY3NGNiZTVlODJkNGQ5NDdkYjI5YTVkMDdiMTY0MWU4OTYxY2YiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6ImFmMTUwOWNjMmU5ZmExZjViOGI4MTVlN2IxZjYwNDYxOWIyYTM1ZWMwZGU1ZTBkMzRiYmZhNjdjYzkxZGJiYzciLCJyZWNvcmRzIjpbeyJhYnNlbmNlX2Jhc2lzIjoibm9uZSIsImF0dGVtcHRfc3RhdGUiOiJkZWxpdmVyeV91bmNlcnRhaW4iLCJlZmZlY3RfY29udHJhY3RfaGFzaCI6InNoYTI1NjphYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X3ZlcmlmaWVkIjpmYWxzZSwiZmluYWxfdmVyZGljdCI6bnVsbCwib2JzZXJ2ZWRfZWZmZWN0IjoidW5rbm93biIsInJlY29uY2lsaWF0aW9uX2FjdGlvbnMiOjAsInJlY29uY2lsaWF0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsInJlY29yZF9raW5kIjoidGVybWluYWxfZWZmZWN0X3N0YXRlIiwicmVzb2x2ZWRfZGVsaXZlcnlfdW5jZXJ0YWludHkiOmZhbHNlLCJyZXN1bHRfaW5kZXgiOjAsInZlcmlmaWNhdGlvbl9wZXJmb3JtZWQiOmZhbHNlLCJ2ZXJpZmljYXRpb25fdGllciI6bnVsbCwidmVyaWZpZXJfaWRlbnRpdHkiOm51bGx9XSwicmVxdWlyZWQiOjEsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZWZmZWN0LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJpZGVudGl0eSI6eyJjb25maXJtZWQiOjEsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiIxMzVjODYzZWRjN2ZhYjE4Y2EyOWI0OTI5M2ZjMjVhMWM5YmQ0ZGUwZTllZDI4ZDRjZWM2ZThkMGE2NzkxMjI5IiwicmVxdWlyZWQiOjEsInJlc3VsdHMiOlt7Im1vZGUiOiJzdHJ1Y3R1cmVkIiwicmVzdWx0X2luZGV4IjowLCJzaWduYWxzIjpbXSwic3RhdHVzIjoidmVyaWZpZWQifV0sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24taWRlbnRpdHktZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sInBvbGljeSI6eyJhZG1pc3Npb25fcG9saWN5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZ292ZXJuZWRfcG9saWN5X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJnb3Zlcm5lZF9ydW50aW1lX2lucHV0c19kaWdlc3QiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsIm1hbmlmZXN0X3NoYTI1NiI6IjRmOTg4ZDAxNWJjZDdjZDdlMDViYTA2NjNkODlmMGE5NjhmMGQ4YWNlOTliMDkwZWJhYmVlNTJhMWM1NTA0MjEiLCJtaW5pbXVtX2VmZmVjdF90aWVyIjoxLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvbGljeS1ldmlkZW5jZS92MSJ9LCJwb3N0Y29uZGl0aW9uIjp7ImNvbmZpcm1lZCI6MCwibWFuaWZlc3Rfc2hhMjU2IjoiM2VhNGM2NGEzMGIwY2YxN2JjYTA0ZjMyNjBlMGE0NzE4YjI3ZjUxZTFmZGMyYmI4OGExODI2MzBhMzRkZDVmZiIsInJlY29yZHMiOlt7ImFjdGlvbl9raW5kIjoiY2xpY2siLCJhY3R1YXRpb25fcGF0aCI6Imd1aSIsImNvbnRyYWN0X2luZGV4IjowLCJjb250cmFjdF9raW5kIjoiZXhwbGljaXRfcHJlZGljYXRlIiwiY29udHJhY3Rfc2hhMjU2IjoiMGFkZTBiNTIwYmMwMDc4OTFjYWNjYTAwMTcxNTc4NDQyYTJhNzRjZWYxNWZmNjRkZGU0NWRmMDdhZmI2NGFlYiIsInJlc3VsdF9pbmRleCI6MCwic3RlcF9jb250cmFjdF9zaGEyNTYiOiIxYTU5NTM2YTQ4OGIxYzQ2YWFjZjBiYTM3OTMwYjg0MDYyYzE0Mjc1NWZhOTM5MTUyZWY1ZTI4YzQwOTQ3MWVkIiwic3RlcF9pbmRleCI6MCwidmVyZGljdCI6InVudmVyaWZpYWJsZSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifV0sInJlcXVpcmVkIjoxLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvc3Rjb25kaXRpb24tZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn19LCJldmlkZW5jZV9ydW5uZXJfc2lnbmVyX3NoYTI1NiI6IjU2NDc1YWE3NTQ2MzQ3NGMwMjg1ZGY1ZGJmMmJjYWI3M2RhNjUxMzU4ODM5ZTliNzc0ODFiMmVhYjEwNzcwOGMiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImV4ZWN1dGlvbl9vdXRjb21lIjp7ImNvbXBlbnNhdGlvbl9hY3Rpb25zIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iLCJpZGVudGl0eSJdLCJleGVjdXRpb25fY29tcGxldGVkIjpmYWxzZSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsInBhc3NlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjowLCJpZGVudGl0eSI6MSwicG9zdGNvbmRpdGlvbiI6MH0sInBvc3Rjb25kaXRpb25fZXZpZGVuY2UiOlt7ImFjdGlvbl9raW5kIjoiY2xpY2siLCJhY3R1YXRpb25fcGF0aCI6Imd1aSIsImNvbnRyYWN0X2luZGV4IjowLCJjb250cmFjdF9raW5kIjoiZXhwbGljaXRfcHJlZGljYXRlIiwiY29udHJhY3Rfc2hhMjU2IjoiMGFkZTBiNTIwYmMwMDc4OTFjYWNjYTAwMTcxNTc4NDQyYTJhNzRjZWYxNWZmNjRkZGU0NWRmMDdhZmI2NGFlYiIsInJlc3VsdF9pbmRleCI6MCwic3RlcF9jb250cmFjdF9zaGEyNTYiOiIxYTU5NTM2YTQ4OGIxYzQ2YWFjZjBiYTM3OTMwYjg0MDYyYzE0Mjc1NWZhOTM5MTUyZWY1ZTI4YzQwOTQ3MWVkIiwic3RlcF9pbmRleCI6MCwidmVyZGljdCI6InVudmVyaWZpYWJsZSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifV0sInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJxdWFsaWZpY2F0aW9uX2V2aWRlbmNlX29ubHkiOmZhbHNlLCJyZXF1aXJlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjoxLCJpZGVudGl0eSI6MSwicG9zdGNvbmRpdGlvbiI6MX0sInZlcnNpb24iOiJvcGVuYWRhcHQuZXhlY3V0aW9uLW91dGNvbWUvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sImV4ZWN1dGlvbl9vdXRjb21lX3NoYTI1NiI6ImZlMzg5YmFiMGViNDQ0N2NlYzE5OTgyNzRmNzI3Y2E1NzAwNDlmZWZkNTU3ZGI3OTkwNDc2OTlmMmY3OGYxNDIiLCJleGVjdXRpb25fcHVycG9zZSI6InByb2R1Y3Rpb24iLCJmaW5hbF9hdXRob3JpdHlfc2VxdWVuY2UiOjAsImZpbmFsX3J1bnRpbWVfZGVsaXZlcnlfc2VxdWVuY2UiOjksImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowM1oiLCJwZXJtaXRfY2hhaW4iOnsiZW50cmllcyI6W3siZGVsaXZlcnlfcmVjZWlwdF9hcnRpZmFjdCI6eyJwYXlsb2FkIjp7ImF1dGhlbnRpY2F0ZWRfcnVubmVyX2lkX3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJhdXRoZW50aWNhdGVkX3Nlc3Npb25faWRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImRlbGl2ZXJlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDFaIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsIm9uZV91c2VfY2xhaW1faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTAiLCJwZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInBlcm1pdF9pZCI6InBlcm1pdDoxIiwicnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6OSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZWNlaXB0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiI4YThmNmQ4ODc1MGNiYjZiM2JkNjYyMDkzYWQyOWUyYmMyMGM2YWI2OTkxZDcwZTU4ODY5MWM4YjNhYjY2MmI5Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZWNlaXB0LWFydGlmYWN0L3YyIiwic2lnbmF0dXJlIjoiQU9LcUNnUzFzMFhhLWl5RTk2Vi15bFB6UWN2MnV2T3N1eWdIenZ1MlJ5VjdGTHNIRl9MVmZxeXg0dTJwdW1sRWwwWWs1WXU3MEpWVWtzWnBhVXBERHciLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImRlbGl2ZXJ5LWF1dGhvcml0eS1lZDI1NTE5LTI0ZjZlZDZhY2JmZTEwMDkiLCJwdWJsaWNfa2V5IjoiS2F5NjRVRzh5dkN5TGhxVTAwMEx4elllVW0wTC9oTElsNVM4a3lLV2JkYz0ifX0sImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3Rfc2hhMjU2IjoiYzAwYTFiYTcwMTVmM2NiZTA1YTc2YWVhYmZmNjE5ZDc5M2U3Mzc1YTMxMGUxOTVmY2JiZDJkMjk0OGYyYjIxNCIsInBlcm1pdF9hcnRpZmFjdCI6eyJwYXlsb2FkIjp7ImFjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJhZG1pc3Npb25fYXJ0aWZhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImF1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV2aWRlbmNlX2lkZW50aXR5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlucHV0X2VkZ2Vfc2VxdWVuY2UiOjEsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwicGVybWl0X2lkIjoicGVybWl0OjEiLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9jaGVja2VkX2F0IjoiMjAyNi0wOC0xOFQxMTo1OTozMFoiLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9leHBpcmVzX2F0IjoiMjAyNi0wOC0yMFQxMTowMDowMFoiLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9yZXZpc2lvbiI6NywicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1wYXlsb2FkL3YyIn0sInBheWxvYWRfc2hhMjU2IjoiYzg3ZjU3NTFhYTliMjc5Y2NlMjNhYTE3OGFmZjk1NmUxOWY2OTBmY2QyZjlmNzBlMGQ5OTk3ODJhNjkwZDZiOCIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWFydGlmYWN0L3YyIiwic2lnbmF0dXJlIjoiMEJ5SGdxRHhRWnJuZ0xJRi02dFpPcTlsWGFtY0FRQzBkb1VUbmVLTlBlUy1TeW9PYlF5T2tBdVBGSzVQWWNOek9iWnhnc2dBTGRXYUlfc2FVQ3BsQmciLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImRlbGl2ZXJ5LWF1dGhvcml0eS1lZDI1NTE5LTI0ZjZlZDZhY2JmZTEwMDkiLCJwdWJsaWNfa2V5IjoiS2F5NjRVRzh5dkN5TGhxVTAwMEx4elllVW0wTC9oTElsNVM4a3lLV2JkYz0ifX0sInBlcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtY2hhaW4tZW50cnkvdjIifV0sInBlcm1pdF9jaGFpbl9zaGEyNTYiOiI1MDQ4ZmU5NDU4ODJkMjgwMjYwMDhkN2U5MTM3YWQwMTZmZGQwMWFlYWY1ODg0ODFmYjViZDBmMjQyYTUwMDQzIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtY2hhaW4vdjIifSwicGVybWl0X2NvdW50IjoxLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9yZXZpc2lvbiI6NywicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZWNlaXB0Ijp7ImF1dGhvcml6YXRpb25fY29uZmlybWVkIjoxLCJhdXRob3JpemF0aW9uX3JlcXVpcmVkIjoxLCJidW5kbGVfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImR1cmF0aW9uX21zIjoxMDAsImVmZmVjdF90aWVyX3JlYWNoZWQiOiJub25lIiwiZWZmZWN0c19jb25maXJtZWQiOjAsImVmZmVjdHNfcmVxdWlyZWQiOjEsImVzdF9jb3N0X21pY3JvdXNkIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iLCJpZGVudGl0eSJdLCJleHRlcm5hbF9uZXR3b3JrX2NhbGxzIjoibm9uZSIsImdlbmVyYXRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiaGVhbHMiOjAsImlkZW50aXR5X2FwcGxpY2FibGUiOjEsImlkZW50aXR5X2FybWVkIjoxLCJpZGVudGl0eV9jb25maXJtZWQiOjEsImlkZW50aXR5X3JlcXVpcmVkIjoxLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsIm92ZXJfaGFsdF9jb3VudCI6MCwicG9zdGNvbmRpdGlvbnNfY29uZmlybWVkIjowLCJwb3N0Y29uZGl0aW9uc19yZXF1aXJlZCI6MSwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInByb3ZlbmFuY2UiOiJwcm9kdWN0aW9uIiwicmVjZWlwdF9idWlsZGVyX3ZlcnNpb24iOiIxLjIuMyIsInJ1bmdfaGlzdG9ncmFtIjp7fSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1ydW4tcmVjZWlwdC92MSIsInNvdXJjZV9yZWNlaXB0X2RpZ2VzdCI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzb3VyY2VfcmVjZWlwdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3NjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnJ1bi1yZXBvcnQvdjEiLCJzdGVwc19vayI6MCwic3RlcHNfdG90YWwiOjEsInN1YnN0cmF0ZSI6IndlYiIsInRyYW5zYWN0aW9uX291dGNvbWUiOiJSRUNPTkNJTElBVElPTl9SRVFVSVJFRCJ9LCJydW5fcmVjZWlwdF9zaGEyNTYiOiIxOGViMmM0ZjQxNTNkYWIwNTNiZjAyN2MyNmY2OGFmYTA2MTMxN2UzYzJlN2Q1YTYwZWU1OGMzZWU2YjNkN2M2IiwicnVuX3JlcG9ydF9vYmplY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bl9yZXBvcnRfb2JqZWN0X3ZlcnNpb24iOiJ2ZXJzaW9uOnJlY29uY2lsaWF0aW9uOjEiLCJydW5fcmVwb3J0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW50aW1lX3N1YnN0cmF0ZSI6IndlYiIsInJ1bnRpbWVfdmFsaWRhdGlvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfc2VxdWVuY2UiOjEsInZlcmlmaWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwid29ya2Zsb3dfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJ3b3JrZmxvd192ZXJzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA0In0sInNpZ25hdHVyZSI6Imd2ZHZuSlNPUzRWazg3M2k4eDBDMmE5MUE0Qm9WOFFvc05xdmMwaUVHMkJOOXpHOUlwamk5dXI1a0VpME5xdW5HVDdLVFZFczhfLUk2VG1TN21ERUJBIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJldmlkZW5jZS1ydW5uZXItZWQyNTUxOS01NjQ3NWFhNzU0NjM0NzRjIiwicHVibGljX2tleSI6IkE2RUh2L1BPRUw0ZGNOMFk1MHZBbVdmazFqQ2JwUTFmSGR5R1pCSlZNYmc9In19","name":"reconciliation-required-nonempty-permit","payload_canonical_sha256":"ef33d988e8895b431f8d06b134cac38fa0b6628b75c0fbe155e30ebb786bd8bf","signature":"gvdvnJSOS4Vk873i8x0C2a91A4BoV8QosNqvc0iEG2BN9zG9Ipji9ur5kEi0NqunGT7KTVEs8_-I6TmS7mDEBA","terminal_verification_artifact_sha256":"d4959d87ddb589221ddcddc6094bf42948c9357b51769239834815036fc36cad"}]} +{ + "key_id": "evidence-runner-ed25519-56475aa75463474c", + "private_key_base64": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", + "public_key_base64": "A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=", + "schema_version": "openadapt.production-terminal-cross-language-vectors/v2", + "signature_domain_base64": "b3BlbmFkYXB0LXByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uLXYyAA==", + "signer_sha256": "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c", + "vectors": [ + { + "callback": { + "artifact_bytes_source": "envelope_canonical_base64", + "outcome": "HALTED_BEFORE_EFFECT", + "report_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "run_id": "00000000-0000-4000-8000-000000000001", + "schema_version": "openadapt.hosted-runner-terminal/v1", + "started": true, + "uncertain_delivery": false + }, + "effect_state": { + "absence_basis": "not_actuated", + "attempt_state": "not_actuated", + "effect_contract_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "effect_verified": false, + "final_verdict": null, + "observed_effect": "absent", + "reconciliation_actions": 0, + "reconciliation_completed": false, + "record_kind": "terminal_effect_state", + "resolved_delivery_uncertainty": false, + "result_index": 0, + "verification_performed": false, + "verification_tier": null, + "verifier_identity": null + }, + "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6IjMyY2QzNjEzYmVlOWJjNjkwNjkzYTM5NWQ5OTMzNTQ3ZjllZWI5ZjZmYWE1ZGJkYmIxNGExMDYyOThiZTQ2NWMiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiODJkYWI1NDI4ZTI1MmQxOGE1OWIwMTc3NzM5ZTAwY2FhZGI3NjJkY2Y1OWY0MDM2ZGVhNGQ3N2MzYzMyMDNjYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6ImViYjU4MzYxNzhlZGZhYzYyOWJkOWJjYTkwZWNkMDE5NTkwOTQ5NjZhMGRkNjNiYWVhN2E2MmY2ZTNiMDRmZjgiLCJyZWNvcmRzIjpbeyJhYnNlbmNlX2Jhc2lzIjoibm90X2FjdHVhdGVkIiwiYXR0ZW1wdF9zdGF0ZSI6Im5vdF9hY3R1YXRlZCIsImVmZmVjdF9jb250cmFjdF9oYXNoIjoic2hhMjU2OmFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJlZmZlY3RfdmVyaWZpZWQiOmZhbHNlLCJmaW5hbF92ZXJkaWN0IjpudWxsLCJvYnNlcnZlZF9lZmZlY3QiOiJhYnNlbnQiLCJyZWNvbmNpbGlhdGlvbl9hY3Rpb25zIjowLCJyZWNvbmNpbGlhdGlvbl9jb21wbGV0ZWQiOmZhbHNlLCJyZWNvcmRfa2luZCI6InRlcm1pbmFsX2VmZmVjdF9zdGF0ZSIsInJlc29sdmVkX2RlbGl2ZXJ5X3VuY2VydGFpbnR5IjpmYWxzZSwicmVzdWx0X2luZGV4IjowLCJ2ZXJpZmljYXRpb25fcGVyZm9ybWVkIjpmYWxzZSwidmVyaWZpY2F0aW9uX3RpZXIiOm51bGwsInZlcmlmaWVyX2lkZW50aXR5IjpudWxsfV0sInJlcXVpcmVkIjoxLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWVmZmVjdC1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiaWRlbnRpdHkiOnsiY29uZmlybWVkIjoxLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiMTM1Yzg2M2VkYzdmYWIxOGNhMjliNDkyOTNmYzI1YTFjOWJkNGRlMGU5ZWQyOGQ0Y2VjNmU4ZDBhNjc5MTIyOSIsInJlcXVpcmVkIjoxLCJyZXN1bHRzIjpbeyJtb2RlIjoic3RydWN0dXJlZCIsInJlc3VsdF9pbmRleCI6MCwic2lnbmFscyI6W10sInN0YXR1cyI6InZlcmlmaWVkIn1dLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWlkZW50aXR5LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJwb2xpY3kiOnsiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX3BvbGljeV9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZ292ZXJuZWRfcnVudGltZV9pbnB1dHNfZGlnZXN0IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiI0Zjk4OGQwMTViY2Q3Y2Q3ZTA1YmEwNjYzZDg5ZjBhOTY4ZjBkOGFjZTk5YjA5MGViYWJlZTUyYTFjNTUwNDIxIiwibWluaW11bV9lZmZlY3RfdGllciI6MSwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb2xpY3ktZXZpZGVuY2UvdjEifSwicG9zdGNvbmRpdGlvbiI6eyJjb25maXJtZWQiOjAsIm1hbmlmZXN0X3NoYTI1NiI6IjNlYTRjNjRhMzBiMGNmMTdiY2EwNGYzMjYwZTBhNDcxOGIyN2Y1MWUxZmRjMmJiODhhMTgyNjMwYTM0ZGQ1ZmYiLCJyZWNvcmRzIjpbeyJhY3Rpb25fa2luZCI6ImNsaWNrIiwiYWN0dWF0aW9uX3BhdGgiOiJndWkiLCJjb250cmFjdF9pbmRleCI6MCwiY29udHJhY3Rfa2luZCI6ImV4cGxpY2l0X3ByZWRpY2F0ZSIsImNvbnRyYWN0X3NoYTI1NiI6IjBhZGUwYjUyMGJjMDA3ODkxY2FjY2EwMDE3MTU3ODQ0MmEyYTc0Y2VmMTVmZjY0ZGRlNDVkZjA3YWZiNjRhZWIiLCJyZXN1bHRfaW5kZXgiOjAsInN0ZXBfY29udHJhY3Rfc2hhMjU2IjoiMWE1OTUzNmE0ODhiMWM0NmFhY2YwYmEzNzkzMGI4NDA2MmMxNDI3NTVmYTkzOTE1MmVmNWUyOGM0MDk0NzFlZCIsInN0ZXBfaW5kZXgiOjAsInZlcmRpY3QiOiJ1bnZlcmlmaWFibGUiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn1dLCJyZXF1aXJlZCI6MSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb3N0Y29uZGl0aW9uLWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9fSwiZXZpZGVuY2VfcnVubmVyX3NpZ25lcl9zaGEyNTYiOiI1NjQ3NWFhNzU0NjM0NzRjMDI4NWRmNWRiZjJiY2FiNzNkYTY1MTM1ODgzOWU5Yjc3NDgxYjJlYWIxMDc3MDhjIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2lnbmVyX3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJleGVjdXRpb25fb3V0Y29tZSI6eyJjb21wZW5zYXRpb25fYWN0aW9ucyI6MCwiZXZpZGVuY2VfY2xhc3NlcyI6WyJhdXRob3JpemF0aW9uIiwiaWRlbnRpdHkiXSwiZXhlY3V0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJub25lIiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJwYXNzZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjEsInBvc3Rjb25kaXRpb24iOjB9LCJwb3N0Y29uZGl0aW9uX2V2aWRlbmNlIjpbeyJhY3Rpb25fa2luZCI6ImNsaWNrIiwiYWN0dWF0aW9uX3BhdGgiOiJndWkiLCJjb250cmFjdF9pbmRleCI6MCwiY29udHJhY3Rfa2luZCI6ImV4cGxpY2l0X3ByZWRpY2F0ZSIsImNvbnRyYWN0X3NoYTI1NiI6IjBhZGUwYjUyMGJjMDA3ODkxY2FjY2EwMDE3MTU3ODQ0MmEyYTc0Y2VmMTVmZjY0ZGRlNDVkZjA3YWZiNjRhZWIiLCJyZXN1bHRfaW5kZXgiOjAsInN0ZXBfY29udHJhY3Rfc2hhMjU2IjoiMWE1OTUzNmE0ODhiMWM0NmFhY2YwYmEzNzkzMGI4NDA2MmMxNDI3NTVmYTkzOTE1MmVmNWUyOGM0MDk0NzFlZCIsInN0ZXBfaW5kZXgiOjAsInZlcmRpY3QiOiJ1bnZlcmlmaWFibGUiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn1dLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjpmYWxzZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicXVhbGlmaWNhdGlvbl9ldmlkZW5jZV9vbmx5IjpmYWxzZSwicmVxdWlyZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MSwiaWRlbnRpdHkiOjEsInBvc3Rjb25kaXRpb24iOjF9LCJ2ZXJzaW9uIjoib3BlbmFkYXB0LmV4ZWN1dGlvbi1vdXRjb21lL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJleGVjdXRpb25fb3V0Y29tZV9zaGEyNTYiOiJmZTM4OWJhYjBlYjQ0NDdjZWMxOTk4Mjc0ZjcyN2NhNTcwMDQ5ZmVmZDU1N2RiNzk5MDQ3Njk5ZjJmNzhmMTQyIiwiZXhlY3V0aW9uX3B1cnBvc2UiOiJwcm9kdWN0aW9uIiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjowLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDNaIiwicGVuZGluZ19wZXJtaXRfY291bnQiOjAsInBlcm1pdF9jaGFpbiI6eyJlbnRyaWVzIjpbXSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjgyZGFiNTQyOGUyNTJkMThhNTliMDE3NzczOWUwMGNhYWRiNzYyZGNmNTlmNDAzNmRlYTRkNzdjM2MzMjAzY2IiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi92MiJ9LCJwZXJtaXRfY291bnQiOjAsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlY2VpcHQiOnsiYXV0aG9yaXphdGlvbl9jb25maXJtZWQiOjEsImF1dGhvcml6YXRpb25fcmVxdWlyZWQiOjEsImJ1bmRsZV9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZHVyYXRpb25fbXMiOjEwMCwiZWZmZWN0X3RpZXJfcmVhY2hlZCI6Im5vbmUiLCJlZmZlY3RzX2NvbmZpcm1lZCI6MCwiZWZmZWN0c19yZXF1aXJlZCI6MSwiZXN0X2Nvc3RfbWljcm91c2QiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiIsImlkZW50aXR5Il0sImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJub25lIiwiZ2VuZXJhdGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJoZWFscyI6MCwiaWRlbnRpdHlfYXBwbGljYWJsZSI6MSwiaWRlbnRpdHlfYXJtZWQiOjEsImlkZW50aXR5X2NvbmZpcm1lZCI6MSwiaWRlbnRpdHlfcmVxdWlyZWQiOjEsIm1vZGVsX2NhbGxzIjowLCJvdXRjb21lIjoiSEFMVEVEIiwib3Zlcl9oYWx0X2NvdW50IjowLCJwb3N0Y29uZGl0aW9uc19jb25maXJtZWQiOjAsInBvc3Rjb25kaXRpb25zX3JlcXVpcmVkIjoxLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjpmYWxzZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicHJvdmVuYW5jZSI6InByb2R1Y3Rpb24iLCJyZWNlaXB0X2J1aWxkZXJfdmVyc2lvbiI6IjEuMi4zIiwicnVuZ19oaXN0b2dyYW0iOnt9LCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXJ1bi1yZWNlaXB0L3YxIiwic291cmNlX3JlY2VpcHRfZGlnZXN0IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9yZWNlaXB0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzb3VyY2Vfc2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucnVuLXJlcG9ydC92MSIsInN0ZXBzX29rIjowLCJzdGVwc190b3RhbCI6MSwic3Vic3RyYXRlIjoid2ViIiwidHJhbnNhY3Rpb25fb3V0Y29tZSI6IkhBTFRFRF9CRUZPUkVfRUZGRUNUIn0sInJ1bl9yZWNlaXB0X3NoYTI1NiI6ImFiNTBhMDA2YTg4OGIzZjkwZTFlNzFlZjIwOTIxYWY3MTdiMmY0MWEzNjc2ZTYwNWZiZGI4MjBjYjZiNTNjYzQiLCJydW5fcmVwb3J0X29iamVjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVuX3JlcG9ydF9vYmplY3RfdmVyc2lvbiI6InZlcnNpb246aGFsdGVkOjEiLCJydW5fcmVwb3J0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW50aW1lX3N1YnN0cmF0ZSI6IndlYiIsInJ1bnRpbWVfdmFsaWRhdGlvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfc2VxdWVuY2UiOjEsInZlcmlmaWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwid29ya2Zsb3dfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJ3b3JrZmxvd192ZXJzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA0In0sInNpZ25hdHVyZSI6IktHQzRqQllpN01ReVlkbFV5TnJ5bGowcmwxaUVNbUFFeHQwUmlxWjV2bGh0Y1lWQzVJOEY1RDZ2V0NkRVlnb09takZNOU5UNjZmVW95ODVsYlFOQkRnIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJldmlkZW5jZS1ydW5uZXItZWQyNTUxOS01NjQ3NWFhNzU0NjM0NzRjIiwicHVibGljX2tleSI6IkE2RUh2L1BPRUw0ZGNOMFk1MHZBbVdmazFqQ2JwUTFmSGR5R1pCSlZNYmc9In19", + "name": "halted-before-effect-zero-permit", + "payload_canonical_sha256": "a2909800fffc776ac70a5f6beb77b7c9941a91e7e2930eb20d9042210dc66001", + "signature": "KGC4jBYi7MQyYdlUyNrylj0rl1iEMmAExt0RiqZ5vlhtcYVC5I8F5D6vWCdEYgoOmjFM9NT66fUoy85lbQNBDg", + "terminal_verification_artifact_sha256": "ef25997f54fdd08ba6cd49533bf23735c35bbac86d195cff19a0df087f82e4d8" + }, + { + "callback": { + "artifact_bytes_source": "envelope_canonical_base64", + "outcome": "RECONCILIATION_REQUIRED", + "report_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "run_id": "00000000-0000-4000-8000-000000000001", + "schema_version": "openadapt.hosted-runner-terminal/v1", + "started": true, + "uncertain_delivery": true + }, + "effect_state": { + "absence_basis": "none", + "attempt_state": "delivery_uncertain", + "effect_contract_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "effect_verified": false, + "final_verdict": null, + "observed_effect": "unknown", + "reconciliation_actions": 0, + "reconciliation_completed": false, + "record_kind": "terminal_effect_state", + "resolved_delivery_uncertainty": false, + "result_index": 0, + "verification_performed": false, + "verification_tier": null, + "verifier_identity": null + }, + "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6IjE1YzBlNzZkYzJlMzIxYTM4NGU1M2VjMTVmZjUzZTc1NTU3YjdjZGU3MWM1YWQyYmEyYjkzZmYyZTk1ODM1YTUiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTliMzk5NzFlMzA1MTA5YzU0Mjk5NDVhNWZhYjQ2NjBiMDlmNDFlN2FjM2ZmZDkyZTM1MDZhZjAwMzM3ZGE5NSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6ImFmMTUwOWNjMmU5ZmExZjViOGI4MTVlN2IxZjYwNDYxOWIyYTM1ZWMwZGU1ZTBkMzRiYmZhNjdjYzkxZGJiYzciLCJyZWNvcmRzIjpbeyJhYnNlbmNlX2Jhc2lzIjoibm9uZSIsImF0dGVtcHRfc3RhdGUiOiJkZWxpdmVyeV91bmNlcnRhaW4iLCJlZmZlY3RfY29udHJhY3RfaGFzaCI6InNoYTI1NjphYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X3ZlcmlmaWVkIjpmYWxzZSwiZmluYWxfdmVyZGljdCI6bnVsbCwib2JzZXJ2ZWRfZWZmZWN0IjoidW5rbm93biIsInJlY29uY2lsaWF0aW9uX2FjdGlvbnMiOjAsInJlY29uY2lsaWF0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsInJlY29yZF9raW5kIjoidGVybWluYWxfZWZmZWN0X3N0YXRlIiwicmVzb2x2ZWRfZGVsaXZlcnlfdW5jZXJ0YWludHkiOmZhbHNlLCJyZXN1bHRfaW5kZXgiOjAsInZlcmlmaWNhdGlvbl9wZXJmb3JtZWQiOmZhbHNlLCJ2ZXJpZmljYXRpb25fdGllciI6bnVsbCwidmVyaWZpZXJfaWRlbnRpdHkiOm51bGx9XSwicmVxdWlyZWQiOjEsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZWZmZWN0LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJpZGVudGl0eSI6eyJjb25maXJtZWQiOjEsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiIxMzVjODYzZWRjN2ZhYjE4Y2EyOWI0OTI5M2ZjMjVhMWM5YmQ0ZGUwZTllZDI4ZDRjZWM2ZThkMGE2NzkxMjI5IiwicmVxdWlyZWQiOjEsInJlc3VsdHMiOlt7Im1vZGUiOiJzdHJ1Y3R1cmVkIiwicmVzdWx0X2luZGV4IjowLCJzaWduYWxzIjpbXSwic3RhdHVzIjoidmVyaWZpZWQifV0sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24taWRlbnRpdHktZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sInBvbGljeSI6eyJhZG1pc3Npb25fcG9saWN5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZ292ZXJuZWRfcG9saWN5X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJnb3Zlcm5lZF9ydW50aW1lX2lucHV0c19kaWdlc3QiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsIm1hbmlmZXN0X3NoYTI1NiI6IjRmOTg4ZDAxNWJjZDdjZDdlMDViYTA2NjNkODlmMGE5NjhmMGQ4YWNlOTliMDkwZWJhYmVlNTJhMWM1NTA0MjEiLCJtaW5pbXVtX2VmZmVjdF90aWVyIjoxLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvbGljeS1ldmlkZW5jZS92MSJ9LCJwb3N0Y29uZGl0aW9uIjp7ImNvbmZpcm1lZCI6MCwibWFuaWZlc3Rfc2hhMjU2IjoiM2VhNGM2NGEzMGIwY2YxN2JjYTA0ZjMyNjBlMGE0NzE4YjI3ZjUxZTFmZGMyYmI4OGExODI2MzBhMzRkZDVmZiIsInJlY29yZHMiOlt7ImFjdGlvbl9raW5kIjoiY2xpY2siLCJhY3R1YXRpb25fcGF0aCI6Imd1aSIsImNvbnRyYWN0X2luZGV4IjowLCJjb250cmFjdF9raW5kIjoiZXhwbGljaXRfcHJlZGljYXRlIiwiY29udHJhY3Rfc2hhMjU2IjoiMGFkZTBiNTIwYmMwMDc4OTFjYWNjYTAwMTcxNTc4NDQyYTJhNzRjZWYxNWZmNjRkZGU0NWRmMDdhZmI2NGFlYiIsInJlc3VsdF9pbmRleCI6MCwic3RlcF9jb250cmFjdF9zaGEyNTYiOiIxYTU5NTM2YTQ4OGIxYzQ2YWFjZjBiYTM3OTMwYjg0MDYyYzE0Mjc1NWZhOTM5MTUyZWY1ZTI4YzQwOTQ3MWVkIiwic3RlcF9pbmRleCI6MCwidmVyZGljdCI6InVudmVyaWZpYWJsZSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifV0sInJlcXVpcmVkIjoxLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvc3Rjb25kaXRpb24tZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn19LCJldmlkZW5jZV9ydW5uZXJfc2lnbmVyX3NoYTI1NiI6IjU2NDc1YWE3NTQ2MzQ3NGMwMjg1ZGY1ZGJmMmJjYWI3M2RhNjUxMzU4ODM5ZTliNzc0ODFiMmVhYjEwNzcwOGMiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImV4ZWN1dGlvbl9vdXRjb21lIjp7ImNvbXBlbnNhdGlvbl9hY3Rpb25zIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iLCJpZGVudGl0eSJdLCJleGVjdXRpb25fY29tcGxldGVkIjpmYWxzZSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsInBhc3NlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjowLCJpZGVudGl0eSI6MSwicG9zdGNvbmRpdGlvbiI6MH0sInBvc3Rjb25kaXRpb25fZXZpZGVuY2UiOlt7ImFjdGlvbl9raW5kIjoiY2xpY2siLCJhY3R1YXRpb25fcGF0aCI6Imd1aSIsImNvbnRyYWN0X2luZGV4IjowLCJjb250cmFjdF9raW5kIjoiZXhwbGljaXRfcHJlZGljYXRlIiwiY29udHJhY3Rfc2hhMjU2IjoiMGFkZTBiNTIwYmMwMDc4OTFjYWNjYTAwMTcxNTc4NDQyYTJhNzRjZWYxNWZmNjRkZGU0NWRmMDdhZmI2NGFlYiIsInJlc3VsdF9pbmRleCI6MCwic3RlcF9jb250cmFjdF9zaGEyNTYiOiIxYTU5NTM2YTQ4OGIxYzQ2YWFjZjBiYTM3OTMwYjg0MDYyYzE0Mjc1NWZhOTM5MTUyZWY1ZTI4YzQwOTQ3MWVkIiwic3RlcF9pbmRleCI6MCwidmVyZGljdCI6InVudmVyaWZpYWJsZSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifV0sInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJxdWFsaWZpY2F0aW9uX2V2aWRlbmNlX29ubHkiOmZhbHNlLCJyZXF1aXJlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjoxLCJpZGVudGl0eSI6MSwicG9zdGNvbmRpdGlvbiI6MX0sInZlcnNpb24iOiJvcGVuYWRhcHQuZXhlY3V0aW9uLW91dGNvbWUvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sImV4ZWN1dGlvbl9vdXRjb21lX3NoYTI1NiI6ImZlMzg5YmFiMGViNDQ0N2NlYzE5OTgyNzRmNzI3Y2E1NzAwNDlmZWZkNTU3ZGI3OTkwNDc2OTlmMmY3OGYxNDIiLCJleGVjdXRpb25fcHVycG9zZSI6InByb2R1Y3Rpb24iLCJmaW5hbF9hdXRob3JpdHlfc2VxdWVuY2UiOjAsImZpbmFsX3J1bnRpbWVfZGVsaXZlcnlfc2VxdWVuY2UiOjAsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowM1oiLCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MSwicGVybWl0X2NoYWluIjp7ImVudHJpZXMiOltdLCJwZW5kaW5nIjp7ImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3QiOm51bGwsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3Rfc2hhMjU2IjpudWxsLCJkZWxpdmVyeV9zdGF0ZSI6IlVOUkVTT0xWRUQiLCJwZXJtaXRfYXJ0aWZhY3QiOnsicGF5bG9hZCI6eyJhY3Rpb25fcmVxdWVzdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2FydGlmYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJhdXRob3JpdHlfc2VxdWVuY2UiOjAsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJldmlkZW5jZV9pZGVudGl0eV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsInBlcm1pdF9pZCI6InBlcm1pdDoxIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfY2hlY2tlZF9hdCI6IjIwMjYtMDgtMThUMTE6NTk6MzBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfZXhwaXJlc19hdCI6IjIwMjYtMDgtMjBUMTE6MDA6MDBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6ImM4N2Y1NzUxYWE5YjI3OWNjZTIzYWExNzhhZmY5NTZlMTlmNjkwZmNkMmY5ZjcwZTBkOTk5NzgyYTY5MGQ2YjgiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6IjBCeUhncUR4UVpybmdMSUYtNnRaT3E5bFhhbWNBUUMwZG9VVG5lS05QZVMtU3lvT2JReU9rQXVQRks1UFljTnpPYlp4Z3NnQUxkV2FJX3NhVUNwbEJnIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJwZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZW5kaW5nLXBlcm1pdC92MiJ9LCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTliMzk5NzFlMzA1MTA5YzU0Mjk5NDVhNWZhYjQ2NjBiMDlmNDFlN2FjM2ZmZDkyZTM1MDZhZjAwMzM3ZGE5NSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWNoYWluL3YyIn0sInBlcm1pdF9jb3VudCI6MSwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVjZWlwdCI6eyJhdXRob3JpemF0aW9uX2NvbmZpcm1lZCI6MSwiYXV0aG9yaXphdGlvbl9yZXF1aXJlZCI6MSwiYnVuZGxlX2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJkdXJhdGlvbl9tcyI6MTAwLCJlZmZlY3RfdGllcl9yZWFjaGVkIjoibm9uZSIsImVmZmVjdHNfY29uZmlybWVkIjowLCJlZmZlY3RzX3JlcXVpcmVkIjoxLCJlc3RfY29zdF9taWNyb3VzZCI6MCwiZXZpZGVuY2VfY2xhc3NlcyI6WyJhdXRob3JpemF0aW9uIiwiaWRlbnRpdHkiXSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJnZW5lcmF0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImhlYWxzIjowLCJpZGVudGl0eV9hcHBsaWNhYmxlIjoxLCJpZGVudGl0eV9hcm1lZCI6MSwiaWRlbnRpdHlfY29uZmlybWVkIjoxLCJpZGVudGl0eV9yZXF1aXJlZCI6MSwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJvdmVyX2hhbHRfY291bnQiOjAsInBvc3Rjb25kaXRpb25zX2NvbmZpcm1lZCI6MCwicG9zdGNvbmRpdGlvbnNfcmVxdWlyZWQiOjEsInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJwcm92ZW5hbmNlIjoicHJvZHVjdGlvbiIsInJlY2VpcHRfYnVpbGRlcl92ZXJzaW9uIjoiMS4yLjMiLCJydW5nX2hpc3RvZ3JhbSI6e30sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcnVuLXJlY2VpcHQvdjEiLCJzb3VyY2VfcmVjZWlwdF9kaWdlc3QiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3JlY2VpcHRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9zY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5ydW4tcmVwb3J0L3YxIiwic3RlcHNfb2siOjAsInN0ZXBzX3RvdGFsIjoxLCJzdWJzdHJhdGUiOiJ3ZWIiLCJ0cmFuc2FjdGlvbl9vdXRjb21lIjoiUkVDT05DSUxJQVRJT05fUkVRVUlSRUQifSwicnVuX3JlY2VpcHRfc2hhMjU2IjoiMThlYjJjNGY0MTUzZGFiMDUzYmYwMjdjMjZmNjhhZmEwNjEzMTdlM2MyZTdkNWE2MGVlNThjM2VlNmIzZDdjNiIsInJ1bl9yZXBvcnRfb2JqZWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW5fcmVwb3J0X29iamVjdF92ZXJzaW9uIjoidmVyc2lvbjpyZWNvbmNpbGlhdGlvbjoxIiwicnVuX3JlcG9ydF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwicnVudGltZV9zdWJzdHJhdGUiOiJ3ZWIiLCJydW50aW1lX3ZhbGlkYXRpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXRlcm1pbmFsLXZlcmlmaWNhdGlvbi92MiIsInRlbmFudF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMiIsInRlcm1pbmFsX3NlcXVlbmNlIjoxLCJ2ZXJpZmllZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIndvcmtmbG93X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwid29ya2Zsb3dfdmVyc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNCJ9LCJzaWduYXR1cmUiOiJkRUdXZ0NDTGZGTm1QNnV5X0JuZ1dZUHJnel9ScmpYeFVULUpkSEwzaTNEclZSTnJ6OWJsWlpzbTBRQXo2cWVHTy1UZkhHT2FLMTVTdUc2ZlIwRGpCdyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZXZpZGVuY2UtcnVubmVyLWVkMjU1MTktNTY0NzVhYTc1NDYzNDc0YyIsInB1YmxpY19rZXkiOiJBNkVIdi9QT0VMNGRjTjBZNTB2QW1XZmsxakNicFExZkhkeUdaQkpWTWJnPSJ9fQ==", + "name": "reconciliation-required-pending-permit", + "payload_canonical_sha256": "2f2af3926539d2bde7a12888bddbdc26aa3193818b82a01743a100d5181581b0", + "signature": "dEGWgCCLfFNmP6uy_BngWYPrgz_RrjXxUT-JdHL3i3DrVRNrz9blZZsm0QAz6qeGO-TfHGOaK15SuG6fR0DjBw", + "terminal_verification_artifact_sha256": "2abcc6c65e0288a235b901169415d59632c8eaacf9b1aac087b1bb0d55ced624" + } + ] +} diff --git a/tests/test_durable_authority_v13.py b/tests/test_durable_authority_v13.py index 0db17584..6fdaaad9 100644 --- a/tests/test_durable_authority_v13.py +++ b/tests/test_durable_authority_v13.py @@ -527,6 +527,22 @@ def test_v2_permit_remains_pending_until_signed_receipt_commits_edge( with pytest.raises(DurableAuthorityBusy, match="remains uncertain"): authority.production_delivery_permit_chain(allow_empty=True) + pending_chain = authority.production_delivery_permit_chain( + allow_pending=True, + receipt_absence_observed_at="2026-08-20T00:00:30Z", + ) + assert pending_chain.entries == () + assert pending_chain.pending is not None + assert pending_chain.pending.permit_artifact == permit.permit_artifact + assert pending_chain.pending.delivery_state == "UNRESOLVED" + assert pending_chain.pending.delivery_receipt_artifact is None + assert pending_chain.pending.delivery_receipt_artifact_sha256 is None + assert pending_chain.pending.actuation_replay_authorized is False + assert pending_chain.pending.receipt_absence_observed_at == ("2026-08-20T00:00:30Z") + assert authority.validate(manifest).delivery_sequence == 0 + with pytest.raises(DurableAuthorityBusy, match="lacks an acknowledgment"): + authority.before_initial_delivery(manifest) + entry = authority.acknowledge_remote_delivery(manifest, permit) assert authority.validate(manifest).delivery_sequence == 1 diff --git a/tests/test_hosted_runner_adapter.py b/tests/test_hosted_runner_adapter.py index 1debe264..11de044f 100644 --- a/tests/test_hosted_runner_adapter.py +++ b/tests/test_hosted_runner_adapter.py @@ -12,6 +12,7 @@ import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from pydantic import ValidationError import openadapt_flow.runner.hosted_adapter as hosted from openadapt_flow.__main__ import _replay_params @@ -751,16 +752,13 @@ def verify_release(*_args): dispatch.delivery_authority_token, ) - first = adapter.execute( - dispatch, - runner_config=tmp_path / "runner.toml", - run_dir=tmp_path / "run", - authority=authority, - ) - - assert first.outcome is TransactionOutcome.RECONCILIATION_REQUIRED - assert first.started is True - assert first.terminal_verification is None + with pytest.raises(RuntimeError, match="signed terminal reconciliation"): + adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run", + authority=authority, + ) assert calls == 1 assert release_checks == 2 @@ -818,25 +816,20 @@ def runner(argv, _run_dir, child_env): dispatch.managed_delivery_authority_url, dispatch.delivery_authority_token, ) - first = adapter.execute( - dispatch, - runner_config=tmp_path / "runner.toml", - run_dir=tmp_path / "run", - authority=authority, - ) - second = adapter.execute( - dispatch, - runner_config=tmp_path / "runner.toml", - run_dir=tmp_path / "run-again", - authority=authority, - ) - - assert first.outcome is TransactionOutcome.RECONCILIATION_REQUIRED - assert first.started is True - assert first.uncertain_delivery is True - assert second.outcome is TransactionOutcome.RECONCILIATION_REQUIRED - assert second.started is True - assert second.uncertain_delivery is True + with pytest.raises(RuntimeError, match="signed terminal reconciliation"): + adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run", + authority=authority, + ) + with pytest.raises(RuntimeError, match="signed terminal reconciliation"): + adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run-again", + authority=authority, + ) assert calls == 1 assert seen_child_env[REMOTE_DISPATCH_SESSION_ID_ENV] == ( dispatch.dispatch_session_id @@ -1201,7 +1194,18 @@ def test_recovery_callback_retains_exact_terminal_v2_envelope(tmp_path, sealed) dispatch_id=dispatch.dispatch_id, run_id=binding.run_id, outcome=TransactionOutcome.VERIFIED, - evidence_batch=(), + evidence_batch=tuple( + hosted.report_events( + _production_report(), + run_id=binding.run_id, + workflow_id=proof.payload.workflow_id, + bundle_digest=proof.payload.bundle_content_digest, + authorization_id="test-authorization", + consequential_steps=1, + effect_covered_consequential_steps=1, + terminal_outcome=TransactionOutcome.VERIFIED.value, + ) + ), terminal_verification=proof, started=True, uncertain_delivery=False, @@ -1260,7 +1264,18 @@ def test_recovery_callback_retains_signed_non_success_terminal_v2( dispatch_id=dispatch.dispatch_id, run_id=binding.run_id, outcome=outcome, - evidence_batch=(), + evidence_batch=tuple( + hosted.report_events( + _production_report(), + run_id=binding.run_id, + workflow_id=proof.payload.workflow_id, + bundle_digest=proof.payload.bundle_content_digest, + authorization_id="test-authorization", + consequential_steps=1, + effect_covered_consequential_steps=1, + terminal_outcome=outcome.value, + ) + ), terminal_verification=proof, started=True, uncertain_delivery=uncertain_delivery, @@ -1287,6 +1302,62 @@ def test_safe_halt_callback_requires_signed_terminal_proof() -> None: ) +def test_terminal_callback_refuses_conflicting_uncertainty_state() -> None: + proof = sign_production_terminal_verification( + _reconciliation_payload(), _private_key() + ) + with pytest.raises(ValueError, match="uncertainty conflicts"): + HostedTerminalEvent( + run_id=proof.payload.run_id, + outcome="RECONCILIATION_REQUIRED", + report_sha256=proof.payload.run_report_sha256, + started=True, + uncertain_delivery=False, + terminal_verification_artifact_bytes_base64=b64encode( + hosted.canonical_json(proof) + ).decode("ascii"), + terminal_verification_artifact_sha256=proof.artifact_sha256(), + ) + + +def test_callback_rejects_confirmed_summary_for_reconciliation() -> None: + proof = sign_production_terminal_verification( + _reconciliation_payload(), _private_key() + ) + terminal = HostedTerminalEvent( + run_id=proof.payload.run_id, + outcome="RECONCILIATION_REQUIRED", + report_sha256=proof.payload.run_report_sha256, + started=True, + uncertain_delivery=True, + terminal_verification_artifact_bytes_base64=b64encode( + hosted.canonical_json(proof) + ).decode("ascii"), + terminal_verification_artifact_sha256=proof.artifact_sha256(), + ) + summary = hosted.report_events( + _production_report(), + run_id=proof.payload.run_id, + workflow_id=proof.payload.workflow_id, + bundle_digest=proof.payload.bundle_content_digest, + authorization_id="test-authorization", + consequential_steps=1, + effect_covered_consequential_steps=1, + terminal_outcome="VERIFIED", + )[-1] + + with pytest.raises(ValidationError, match="conflicts with terminal outcome"): + CallbackRequest( + dispatch_id="11111111-1111-4111-8111-111111111111", + runner_session_id="22222222-2222-4222-8222-222222222222", + idempotency_key="callback-test-key", + lease_token="oal_" + "a" * 64, + product_release_admission_sha256="b" * 64, + workflow_admission_sha256="c" * 64, + events=(summary, terminal.model_dump(mode="json")), + ) + + def test_callback_refuses_terminal_proof_for_a_different_run(tmp_path, sealed) -> None: workflow, _ = sealed dispatch = _hosted_dispatch(workflow) diff --git a/tests/test_run_receipt.py b/tests/test_run_receipt.py index 7e6ba54b..b99390ca 100644 --- a/tests/test_run_receipt.py +++ b/tests/test_run_receipt.py @@ -241,6 +241,8 @@ def _terminal_payload_from_report() -> tuple[ execution_authority_signer_sha256=(chain.entries[0].authority_signer_sha256), permit_chain=chain, permit_count=1, + acknowledged_permit_count=1, + pending_permit_count=0, final_authority_sequence=0, final_runtime_delivery_sequence=9, workflow_contract_sha256=prepared.execution_outcome.workflow_contract_sha256, @@ -270,6 +272,7 @@ def _terminal_expected( "authenticated_runner_id_sha256", "authenticated_session_id_sha256", "acknowledged_one_use_claim_ids", + "pending_permit_artifact_sha256", } } values["permit_chain_sha256"] = payload.permit_chain.permit_chain_sha256 @@ -282,6 +285,7 @@ def _terminal_expected( values["acknowledged_one_use_claim_ids"] = tuple( entry.one_use_claim_id for entry in payload.permit_chain.entries ) + values["pending_permit_artifact_sha256"] = None return ProductionTerminalVerificationExpected.model_validate(values) diff --git a/tests/test_terminal_verification_v2.py b/tests/test_terminal_verification_v2.py index e96245df..08107dbb 100644 --- a/tests/test_terminal_verification_v2.py +++ b/tests/test_terminal_verification_v2.py @@ -39,6 +39,7 @@ ProductionExecutionOutcome, ProductionIdentityEvidenceManifest, ProductionIdentityResult, + ProductionPendingDeliveryPermit, ProductionPolicyEvidenceManifest, ProductionPostconditionEvidenceManifest, ProductionRunReceipt, @@ -469,6 +470,15 @@ def _permit_chain() -> ProductionDeliveryPermitChain: return ProductionDeliveryPermitChain.build((_permit_entry(),)) +def _pending_permit_chain() -> ProductionDeliveryPermitChain: + permit = _permit_entry().permit_artifact + pending = ProductionPendingDeliveryPermit.build( + permit, + receipt_absence_observed_at="2026-08-18T12:00:02Z", + ) + return ProductionDeliveryPermitChain.build((), pending=pending) + + def _manifests( chain: ProductionDeliveryPermitChain, ) -> ProductionEvidenceManifests: @@ -685,6 +695,8 @@ def _halted_payload() -> ProductionTerminalVerificationPayload: execution_authority_signer_sha256=SHA_C, permit_chain=chain, permit_count=0, + acknowledged_permit_count=0, + pending_permit_count=0, final_authority_sequence=0, final_runtime_delivery_sequence=0, workflow_contract_sha256=SHA_A, @@ -704,7 +716,7 @@ def _halted_payload() -> ProductionTerminalVerificationPayload: def _reconciliation_payload() -> ProductionTerminalVerificationPayload: - chain = _permit_chain() + chain = _pending_permit_chain() receipt = _reconciliation_receipt() outcome = _reconciliation_outcome() return ProductionTerminalVerificationPayload( @@ -726,11 +738,13 @@ def _reconciliation_payload() -> ProductionTerminalVerificationPayload: qualification_signer_registry_sha256=SHA_E, qualification_signer_registry_revision=7, execution_authority_sha256=SHA_A, - execution_authority_signer_sha256=chain.entries[0].authority_signer_sha256, + execution_authority_signer_sha256=chain.pending.authority_signer_sha256, permit_chain=chain, permit_count=1, + acknowledged_permit_count=0, + pending_permit_count=1, final_authority_sequence=0, - final_runtime_delivery_sequence=9, + final_runtime_delivery_sequence=0, workflow_contract_sha256=SHA_A, execution_outcome=outcome, execution_outcome_sha256=outcome.artifact_sha256(), @@ -772,6 +786,8 @@ def _payload() -> ProductionTerminalVerificationPayload: execution_authority_signer_sha256=(chain.entries[0].authority_signer_sha256), permit_chain=chain, permit_count=1, + acknowledged_permit_count=1, + pending_permit_count=0, final_authority_sequence=0, final_runtime_delivery_sequence=9, workflow_contract_sha256=SHA_A, @@ -832,6 +848,13 @@ def _expected( execution_authority_signer_sha256=(payload.execution_authority_signer_sha256), permit_chain_sha256=payload.permit_chain.permit_chain_sha256, permit_count=payload.permit_count, + acknowledged_permit_count=payload.acknowledged_permit_count, + pending_permit_count=payload.pending_permit_count, + pending_permit_artifact_sha256=( + payload.permit_chain.pending.permit_artifact_sha256 + if payload.permit_chain.pending is not None + else None + ), final_authority_sequence=payload.final_authority_sequence, final_runtime_delivery_sequence=payload.final_runtime_delivery_sequence, authenticated_runner_id_sha256=authenticated_runner_id_sha256, @@ -960,12 +983,93 @@ def test_terminal_v2_signs_and_verifies_reconciliation_proof() -> None: assert digest == envelope.artifact_sha256() assert payload.run_receipt.transaction_outcome == "RECONCILIATION_REQUIRED" assert payload.permit_count == 1 + assert payload.acknowledged_permit_count == 0 + assert payload.pending_permit_count == 1 + assert payload.final_runtime_delivery_sequence == 0 + assert payload.permit_chain.pending is not None + assert payload.permit_chain.pending.delivery_state == "UNRESOLVED" + assert payload.permit_chain.pending.delivery_receipt_artifact is None + assert payload.permit_chain.pending.actuation_replay_authorized is False record = payload.evidence_manifests.effect.records[0] assert isinstance(record, ProductionTerminalEffectState) assert record.attempt_state == "delivery_uncertain" assert record.observed_effect == "unknown" +def test_pending_delivery_chain_rebuilds_from_exact_retained_permit() -> None: + chain = _pending_permit_chain() + assert chain.pending is not None + + rebuilt = rebuild_production_delivery_permit_chain_from_artifacts( + (), + pending_permit_artifact=chain.pending.permit_artifact.canonical_bytes(), + receipt_absence_observed_at=chain.pending.receipt_absence_observed_at, + ) + + assert rebuilt == chain + with pytest.raises( + ProductionTerminalVerificationError, + match="pending delivery binding is incomplete", + ): + rebuild_production_delivery_permit_chain_from_artifacts( + (), + pending_permit_artifact=chain.pending.permit_artifact.canonical_bytes(), + ) + + +def test_pending_reconciliation_refuses_effect_absence_or_replay() -> None: + payload = _reconciliation_payload() + assert payload.permit_chain.pending is not None + pending = payload.permit_chain.pending.model_dump(mode="json") + + pending["actuation_replay_authorized"] = True + with pytest.raises(ValidationError): + ProductionPendingDeliveryPermit.model_validate(pending) + + data = payload.model_dump(mode="json") + effect = data["evidence_manifests"]["effect"] + effect["records"][0]["observed_effect"] = "absent" + effect["records"][0]["absence_basis"] = "not_actuated" + with pytest.raises(ValidationError): + ProductionTerminalVerificationPayload.model_validate(data) + + +def test_terminal_acceptor_rechecks_pending_permit_digest() -> None: + payload = _reconciliation_payload() + envelope = sign_production_terminal_verification(payload, _private_key()) + expected = _expected(payload).model_copy( + update={"pending_permit_artifact_sha256": SHA_A} + ) + + with pytest.raises( + ProductionTerminalVerificationError, + match="pending permit", + ): + verify_production_terminal_verification( + envelope, + expected=expected, + now=NOW, + ) + + +def test_verified_terminal_refuses_a_pending_permit() -> None: + success = _payload() + pending_chain = _pending_permit_chain() + data = success.model_dump(mode="json") + data.update( + { + "permit_chain": pending_chain.model_dump(mode="json"), + "acknowledged_permit_count": 0, + "pending_permit_count": 1, + "final_runtime_delivery_sequence": 0, + "evidence_manifests": _manifests(pending_chain).model_dump(mode="json"), + } + ) + + with pytest.raises(ValidationError, match="VERIFIED proof is incomplete"): + ProductionTerminalVerificationPayload.model_validate(data) + + @pytest.mark.parametrize( "transaction_outcome", ["HALTED_BEFORE_EFFECT", "RECONCILIATION_REQUIRED"], @@ -977,7 +1081,7 @@ def test_terminal_v2_builds_non_success_proof_from_exact_run_report( chain = ( ProductionDeliveryPermitChain.build(()) if transaction_outcome == "HALTED_BEFORE_EFFECT" - else _permit_chain() + else _pending_permit_chain() ) context = ProductionTerminalVerificationContext( run_id=IDS["run_id"], @@ -1004,7 +1108,11 @@ def test_terminal_v2_builds_non_success_proof_from_exact_run_report( execution_authority_id=IDS["execution_authority_id"], execution_authority_sha256=SHA_A, execution_authority_signer_sha256=( - chain.entries[0].authority_signer_sha256 if chain.entries else SHA_C + chain.pending.authority_signer_sha256 + if chain.pending is not None + else chain.entries[0].authority_signer_sha256 + if chain.entries + else SHA_C ), permit_chain=chain, run_report_object_version="version:terminal-report:1", @@ -1312,7 +1420,7 @@ def test_non_success_terminal_cross_language_vectors_are_exact() -> None: ) payloads = { "halted-before-effect-zero-permit": _halted_payload(), - "reconciliation-required-nonempty-permit": _reconciliation_payload(), + "reconciliation-required-pending-permit": _reconciliation_payload(), } for vector in fixture["vectors"]: From 1d64964a3b922f3d6da3bfa65dc5431ce482a9fd Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Thu, 27 Aug 2026 15:45:12 -0400 Subject: [PATCH 7/9] fix(hosted): fence managed result-loss terminalization --- openadapt_flow/ir.py | 165 +++ openadapt_flow/runner/__init__.py | 4 + openadapt_flow/runner/hosted_adapter.py | 1302 +++++++++++++++-- openadapt_flow/terminal_verification_v2.py | 479 +++++- openadapt_flow/transaction.py | 8 + public-artifacts.json | 2 +- ...nerate_terminal_verification_v2_vectors.py | 90 +- ...inal_verification_v2_terminal_vectors.json | 175 +++ tests/test_hosted_runner_adapter.py | 654 ++++++++- tests/test_terminal_verification_v2.py | 419 +++++- 10 files changed, 3192 insertions(+), 106 deletions(-) diff --git a/openadapt_flow/ir.py b/openadapt_flow/ir.py index b6a8ffb2..6f337eb1 100644 --- a/openadapt_flow/ir.py +++ b/openadapt_flow/ir.py @@ -3371,6 +3371,145 @@ class EffectJournalEntry(BaseModel): collateral_reconciliation_actions: int = Field(default=0, ge=0) +MANAGED_RESULT_LOSS_DOMAIN: Final[bytes] = b"OpenAdapt managed result loss v1\0" +MANAGED_RESULT_LOSS_IDEMPOTENCY_DOMAIN: Final[bytes] = ( + b"OpenAdapt managed result loss idempotency v1\0" +) + + +def managed_result_loss_idempotency_sha256(idempotency_key: str) -> str: + """Bind a retained hosted idempotency key without exposing the key.""" + + if not isinstance(idempotency_key, str) or not idempotency_key: + raise ValueError("managed result loss idempotency key is invalid") + return hashlib.sha256( + MANAGED_RESULT_LOSS_IDEMPOTENCY_DOMAIN + idempotency_key.encode("utf-8") + ).hexdigest() + + +class ManagedResultLossEvidence(BaseModel): + """Sealed PHI-free evidence that a managed child result was lost. + + This record does not describe the business effect. It binds the outer + runner's exact retained dispatch and unresolved permit state, and it makes + every prohibited inference explicit. The signed terminal proof can then + require reconciliation without claiming that delivery did not occur. + """ + + model_config = ConfigDict(extra="forbid", frozen=True) + + schema_version: Literal["openadapt.managed-result-loss/v1"] = ( + "openadapt.managed-result-loss/v1" + ) + report_provenance: Literal["outer_runner_recovery"] = "outer_runner_recovery" + loss_code: Literal[ + "runner_exception", + "report_missing", + "recovered_after_restart", + ] + child_started_at: str = Field( + pattern=r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$" + ) + child_start_evidence_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + run_store_identity_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + observed_at: str = Field( + pattern=r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$" + ) + run_id: str = Field( + pattern=( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-" + r"[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + ) + ) + flow_run_id_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + dispatch_id: str = Field( + pattern=( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-" + r"[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + ) + ) + dispatch_session_id: str = Field( + pattern=( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-" + r"[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + ) + ) + managed_dispatch_binding_sha256: str = Field(pattern=r"^sha256:[a-f0-9]{64}$") + idempotency_key_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + authenticated_runner_id_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + authenticated_session_id_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + execution_authority_id: str = Field( + pattern=( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-" + r"[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + ) + ) + execution_authority_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + execution_authority_signer_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + delivery_result_loss_closure_artifact_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + pending_permit_artifact_sha256: Optional[str] = Field( + default=None, + pattern=r"^[a-f0-9]{64}$", + ) + run_request_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + pending_action_request_sha256: Optional[str] = Field( + default=None, + pattern=r"^[a-f0-9]{64}$", + ) + delivery_state: Literal["CLOSED_UNRESOLVED_RESULT_LOSS"] = ( + "CLOSED_UNRESOLVED_RESULT_LOSS" + ) + child_started: Literal[True] = True + child_report_retained: Literal[False] = False + effect_absence_claimed: Literal[False] = False + not_received_claimed: Literal[False] = False + blind_retry_authorized: Literal[False] = False + actuation_replay_authorized: Literal[False] = False + evidence_sha256: str = Field(pattern=r"^[a-f0-9]{64}$") + + def _digest_payload(self) -> dict[str, Any]: + payload = self.model_dump(mode="json") + payload.pop("evidence_sha256", None) + return payload + + def computed_sha256(self) -> str: + encoded = json.dumps( + self._digest_payload(), + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ).encode("utf-8") + return hashlib.sha256(MANAGED_RESULT_LOSS_DOMAIN + encoded).hexdigest() + + @model_validator(mode="after") + def _closed_result_loss(self) -> "ManagedResultLossEvidence": + if hashlib.sha256(self.run_id.encode("utf-8")).hexdigest() != ( + self.flow_run_id_sha256 + ): + raise ValueError("managed result loss run identity digest is invalid") + if self.evidence_sha256 != self.computed_sha256(): + raise ValueError("managed result loss evidence digest is invalid") + if (self.pending_permit_artifact_sha256 is None) != ( + self.pending_action_request_sha256 is None + ): + raise ValueError("managed result loss pending binding is incomplete") + child_started = datetime.fromisoformat(self.child_started_at[:-1] + "+00:00") + observed = datetime.fromisoformat(self.observed_at[:-1] + "+00:00") + if child_started > observed: + raise ValueError("managed result loss chronology is invalid") + return self + + @classmethod + def create(cls, **values: Any) -> "ManagedResultLossEvidence": + candidate = cls.model_construct(**values, evidence_sha256="0" * 64) + return cls.model_validate( + { + **candidate._digest_payload(), + "evidence_sha256": candidate.computed_sha256(), + } + ) + + class RunReport(BaseModel): workflow_name: str started_at: str @@ -3466,6 +3605,14 @@ class RunReport(BaseModel): "delta). PHI-free; the substrate the reconciliation model reads." ), ) + managed_result_loss: Optional[ManagedResultLossEvidence] = Field( + default=None, + exclude_if=lambda value: value is None, + description=( + "Sealed outer-runner evidence that the managed child result was " + "lost after execution started. It authorizes reconciliation only." + ), + ) idempotency_key: Optional[str] = Field( default=None, description=( @@ -3721,6 +3868,24 @@ def _validate_outcome_envelope_binding(self) -> "RunReport": ) envelope = self.outcome_envelope + if self.managed_result_loss is not None: + if ( + self.execution_outcome != "HALTED" + or self.production_eligible + or self.execution_completed is not False + or self.success + or self.transaction_outcome != "RECONCILIATION_REQUIRED" + or self.transaction_billable is not False + or self.transaction_platform_fault is not False + or len(self.results) != 1 + or self.results[0].step_id != "" + or self.results[0].ok + or self.results[0].delivery_attempted is not None + or self.results[0].effect_contract_hashes + or self.results[0].effect_evidence + or self.results[0].effect_verified is not None + ): + raise ValueError("managed result loss report contract is invalid") if envelope is None: return self if self.execution_outcome != envelope.outcome: diff --git a/openadapt_flow/runner/__init__.py b/openadapt_flow/runner/__init__.py index 3d975663..adf17415 100644 --- a/openadapt_flow/runner/__init__.py +++ b/openadapt_flow/runner/__init__.py @@ -59,6 +59,8 @@ HostedRunnerTransport, HostedRunResult, PollRequest, + ProductionDeliveryResultLossClosureRequest, + ProductionDeliveryResultLossClosureResult, RegisterCapabilities, RegisterRequest, RegisterResponse, @@ -110,6 +112,8 @@ "Refusal", "RefusalCode", "PollRequest", + "ProductionDeliveryResultLossClosureRequest", + "ProductionDeliveryResultLossClosureResult", "RUNNER_RENEWAL_HEADER", "RegisterCapabilities", "RegisterRequest", diff --git a/openadapt_flow/runner/hosted_adapter.py b/openadapt_flow/runner/hosted_adapter.py index 7fa80f08..107b70b6 100644 --- a/openadapt_flow/runner/hosted_adapter.py +++ b/openadapt_flow/runner/hosted_adapter.py @@ -12,6 +12,7 @@ import json import os import re +import secrets import stat import subprocess from base64 import b64decode, b64encode @@ -25,7 +26,15 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from pydantic import BaseModel, ConfigDict, Field, model_validator -from openadapt_flow.ir import RunReport, Workflow +from openadapt_flow.bundle_validation import compute_parameter_schema_digest +from openadapt_flow.execution_profiles import stamp_execution_outcome +from openadapt_flow.ir import ( + ManagedResultLossEvidence, + RunReport, + StepResult, + Workflow, + managed_result_loss_idempotency_sha256, +) from openadapt_flow.private_file import ( PrivateFileAclError, windows_descriptor_has_private_acl, @@ -35,6 +44,7 @@ ProductionQualificationGuard, _read_private_json, ) +from openadapt_flow.qualification import workflow_contract_sha256 from openadapt_flow.qualification_admission_v2 import ( QualificationAdmissionEnvelope, QualificationAdmissionExpected, @@ -72,12 +82,16 @@ ) from openadapt_flow.runtime.durable.checkpoint import CheckpointStore from openadapt_flow.terminal_verification_v2 import ( + RESULT_LOSS_CLOSURE_REQUEST_DOMAIN, + ProductionDeliveryPermitChain, + ProductionDeliveryResultLossClosureArtifact, ProductionTerminalVerificationContext, ProductionTerminalVerificationEnvelope, ProductionTerminalVerificationExpected, build_production_terminal_verification, evidence_runner_signer_sha256, prepare_production_terminal_evidence, + verify_production_delivery_result_loss_closure_binding, verify_production_terminal_verification_from_report, ) from openadapt_flow.transaction import ( @@ -95,6 +109,12 @@ _SAFE_ID = r"^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,199}$" _UTC_SECONDS = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$") _MAX_ARTIFACT_BYTES = 2 * 1024 * 1024 +_MAX_RESULT_LOSS_CHAIN_BYTES = 1024 * 1024 +_MAX_RESULT_LOSS_SNAPSHOT_BYTES = 3 * 1024 * 1024 +_CHILD_START_DOMAIN = b"OpenAdapt managed child start v1\0" +_RUN_STORE_IDENTITY_DOMAIN = b"OpenAdapt managed run store identity v1\0" +_RESULT_LOSS_SNAPSHOT_DOMAIN = b"OpenAdapt managed result loss snapshot v1\0" +_MAX_TERMINAL_STATE_BYTES = 2 * (4 * ((_MAX_ARTIFACT_BYTES + 2) // 3)) + 4096 # The current runner credential is never part of a request model. Desktop may # project it into this header only for POST /api/runners/register on the exact @@ -283,6 +303,197 @@ class HostedRecoveryBinding(_Closed): authorization_id: str = Field(min_length=1, max_length=128) +class ManagedChildStartEvidence(_Closed): + """Durable outer-runner boundary written before managed child entry.""" + + schema_version: Literal["openadapt.managed-child-start/v1"] = ( + "openadapt.managed-child-start/v1" + ) + started_at: str + dispatch_id: str = Field(pattern=_UUID) + dispatch_session_id: str = Field(pattern=_UUID) + run_id: str = Field(pattern=_UUID) + managed_dispatch_binding_sha256: str = Field(pattern=r"^sha256:[a-f0-9]{64}$") + authenticated_runner_id_sha256: str = Field(pattern=_HEX64) + authenticated_session_id_sha256: str = Field(pattern=_HEX64) + execution_authority_id: str = Field(pattern=_UUID) + execution_authority_sha256: str = Field(pattern=_HEX64) + execution_authority_signer_sha256: str = Field(pattern=_HEX64) + run_store_identity_sha256: str = Field(pattern=_HEX64) + marker_sha256: str = Field(pattern=_HEX64) + + def computed_sha256(self) -> str: + payload = self.model_dump(mode="json") + payload.pop("marker_sha256", None) + return hashlib.sha256(_CHILD_START_DOMAIN + canonical_json(payload)).hexdigest() + + @model_validator(mode="after") + def _closed_marker(self) -> "ManagedChildStartEvidence": + _utc_seconds(self.started_at, label="managed child start") + if self.marker_sha256 != self.computed_sha256(): + raise ValueError("managed child start digest is invalid") + return self + + @classmethod + def create(cls, **values: Any) -> "ManagedChildStartEvidence": + candidate = cls.model_construct(**values, marker_sha256="0" * 64) + payload = candidate.model_dump(mode="json") + payload["marker_sha256"] = candidate.computed_sha256() + return cls.model_validate(payload) + + +class ProductionDeliveryResultLossClosureRequest(_Closed): + """Credential-free request for one monotonic hosted result-loss fence.""" + + schema_version: Literal[ + "openadapt.production-delivery-result-loss-closure-request/v2" + ] = "openadapt.production-delivery-result-loss-closure-request/v2" + child_start_evidence: ManagedChildStartEvidence + expected_closure_sequence: Literal[0] = 0 + result_loss_observed_at: str + + @model_validator(mode="after") + def _closed_request(self) -> "ProductionDeliveryResultLossClosureRequest": + observed = _utc_seconds( + self.result_loss_observed_at, + label="result loss observation", + ) + started = _utc_seconds( + self.child_start_evidence.started_at, + label="managed child start", + ) + if started > observed: + raise ValueError("result-loss closure request chronology is invalid") + return self + + def canonical_bytes(self) -> bytes: + return canonical_json(self) + + def request_sha256(self) -> str: + return hashlib.sha256( + RESULT_LOSS_CLOSURE_REQUEST_DOMAIN + self.canonical_bytes() + ).hexdigest() + + +class ProductionDeliveryResultLossClosureResult(_Closed): + """Exact Cloud authority closure and its atomically frozen permit chain.""" + + schema_version: Literal[ + "openadapt.production-delivery-result-loss-closure-result/v2" + ] = "openadapt.production-delivery-result-loss-closure-result/v2" + status: Literal["closed"] = "closed" + closure_artifact_bytes_base64: str = Field(max_length=2_796_204) + closure_artifact_sha256: str = Field(pattern=_HEX64) + permit_chain_bytes_base64: str = Field(max_length=2_796_204) + permit_chain_sha256: str = Field(pattern=_HEX64) + + @staticmethod + def _decode_canonical( + value: str, + *, + label: str, + maximum_bytes: int = _MAX_ARTIFACT_BYTES, + ) -> bytes: + try: + raw = b64decode(value, validate=True) + except (TypeError, ValueError) as exc: + raise ValueError(f"{label} is not canonical base64") from exc + if ( + not raw + or len(raw) > maximum_bytes + or b64encode(raw).decode("ascii") != value + ): + raise ValueError(f"{label} is not canonical base64") + return raw + + def artifacts( + self, + ) -> tuple[ + ProductionDeliveryResultLossClosureArtifact, + ProductionDeliveryPermitChain, + ]: + closure_raw = self._decode_canonical( + self.closure_artifact_bytes_base64, + label="result-loss closure artifact", + ) + chain_raw = self._decode_canonical( + self.permit_chain_bytes_base64, + label="result-loss closure permit chain", + maximum_bytes=_MAX_RESULT_LOSS_CHAIN_BYTES, + ) + try: + closure = ProductionDeliveryResultLossClosureArtifact.model_validate_json( + closure_raw + ) + chain = ProductionDeliveryPermitChain.model_validate_json(chain_raw) + except ValueError as exc: + raise ValueError("result-loss closure response is invalid") from exc + if ( + canonical_json(closure) != closure_raw + or canonical_json(chain) != chain_raw + or hashlib.sha256(closure_raw).hexdigest() != self.closure_artifact_sha256 + or closure.artifact_sha256() != self.closure_artifact_sha256 + or chain.permit_chain_sha256 != self.permit_chain_sha256 + or closure.payload.permit_chain_sha256 != self.permit_chain_sha256 + ): + raise ValueError("result-loss closure response binding is invalid") + return closure, chain + + @model_validator(mode="after") + def _closed_result(self) -> "ProductionDeliveryResultLossClosureResult": + self.artifacts() + return self + + +class ManagedResultLossSnapshot(_Closed): + """One local CAS record for the loss evidence and exact permit-chain view.""" + + schema_version: Literal["openadapt.managed-result-loss-snapshot/v1"] = ( + "openadapt.managed-result-loss-snapshot/v1" + ) + evidence: ManagedResultLossEvidence + permit_chain: ProductionDeliveryPermitChain + closure_artifact: ProductionDeliveryResultLossClosureArtifact + snapshot_sha256: str = Field(pattern=_HEX64) + + def computed_sha256(self) -> str: + payload = self.model_dump(mode="json") + payload.pop("snapshot_sha256", None) + return hashlib.sha256( + _RESULT_LOSS_SNAPSHOT_DOMAIN + canonical_json(payload) + ).hexdigest() + + @model_validator(mode="after") + def _closed_snapshot(self) -> "ManagedResultLossSnapshot": + if ( + self.snapshot_sha256 != self.computed_sha256() + or self.evidence.delivery_result_loss_closure_artifact_sha256 + != self.closure_artifact.artifact_sha256() + or self.closure_artifact.payload.permit_chain_sha256 + != self.permit_chain.permit_chain_sha256 + ): + raise ValueError("managed result loss snapshot is invalid") + return self + + @classmethod + def create( + cls, + *, + evidence: ManagedResultLossEvidence, + permit_chain: ProductionDeliveryPermitChain, + closure_artifact: ProductionDeliveryResultLossClosureArtifact, + ) -> "ManagedResultLossSnapshot": + candidate = cls.model_construct( + evidence=evidence, + permit_chain=permit_chain, + closure_artifact=closure_artifact, + snapshot_sha256="0" * 64, + ) + payload = candidate.model_dump(mode="json") + payload["snapshot_sha256"] = candidate.computed_sha256() + return cls.model_validate(payload) + + class HostedTerminalEvent(_Closed): schema_version: Literal["openadapt.hosted-runner-terminal/v1"] = ( "openadapt.hosted-runner-terminal/v1" @@ -323,8 +534,6 @@ def _terminal_outcome_requires_exact_proof(self) -> "HostedTerminalEvent": and not has_proof ): raise ValueError(f"{self.outcome} requires exact terminal verification") - if self.uncertain_delivery != (self.outcome == "RECONCILIATION_REQUIRED"): - raise ValueError("terminal uncertainty conflicts with its outcome") if ( self.outcome not in { @@ -362,6 +571,11 @@ def _terminal_outcome_requires_exact_proof(self) -> "HostedTerminalEvent": raise ValueError("terminal verification names a different run report") if proof.payload.run_receipt.transaction_outcome != self.outcome: raise ValueError("terminal verification names a different outcome") + expected_uncertainty = proof.payload.pending_permit_count == 1 + else: + expected_uncertainty = False + if self.uncertain_delivery != expected_uncertainty: + raise ValueError("terminal uncertainty conflicts with its signed proof") return self @@ -394,10 +608,12 @@ def _closed_terminal(self) -> "HostedRunResult": TransactionOutcome.RECONCILIATION_REQUIRED, }: raise ValueError("terminal outcome cannot carry a signed v2 proof") - if self.uncertain_delivery != ( - self.outcome is TransactionOutcome.RECONCILIATION_REQUIRED - ): - raise ValueError("uncertain delivery has an invalid terminal outcome") + expected_uncertainty = bool( + self.terminal_verification is not None + and self.terminal_verification.payload.pending_permit_count == 1 + ) + if self.uncertain_delivery != expected_uncertainty: + raise ValueError("uncertain delivery conflicts with its signed proof") if self.terminal_verification is not None and ( self.terminal_verification.payload.run_id != self.run_id or self.terminal_verification.payload.run_report_sha256 @@ -483,6 +699,13 @@ def register(self, request: RegisterRequest) -> RegisterResponse: ... def poll(self, request: PollRequest) -> HostedDispatch | None: ... + def close_result_loss( + self, + run_id: str, + lease_token: str, + request: ProductionDeliveryResultLossClosureRequest, + ) -> ProductionDeliveryResultLossClosureResult: ... + def callback(self, run_id: str, request: CallbackRequest) -> CallbackResponse: ... @@ -1018,6 +1241,515 @@ def _write_private_bytes(path: Path, raw: bytes) -> Path: os.close(descriptor) return path + @staticmethod + def _write_private_bytes_atomic_exclusive(path: Path, raw: bytes) -> Path: + """Publish complete owner-only bytes once, without an overwrite window.""" + + temporary = path.with_name( + f".{path.name}.{os.getpid()}.{secrets.token_hex(8)}.tmp" + ) + try: + HostedRunnerAdapter._write_private_bytes(temporary, raw) + try: + os.link(temporary, path) + except FileExistsError: + raise + try: + directory = os.open(path.parent, os.O_RDONLY) + except OSError: + directory = None + if directory is not None: + try: + os.fsync(directory) + finally: + os.close(directory) + finally: + try: + temporary.unlink() + except FileNotFoundError: + pass + return path + + @staticmethod + def _run_store_identity_sha256(run_dir: Path) -> str: + resolved = Path(run_dir).resolve(strict=True) + value = resolved.lstat() + payload = { + "canonical_run_dir": str(resolved), + "device": int(value.st_dev), + "inode": int(value.st_ino), + } + return hashlib.sha256( + _RUN_STORE_IDENTITY_DOMAIN + canonical_json(payload) + ).hexdigest() + + def _write_child_start_evidence( + self, + *, + dispatch: HostedDispatch, + run_dir: Path, + dispatch_binding_sha256: str, + ) -> ManagedChildStartEvidence: + now = datetime.now(timezone.utc).replace(microsecond=0) + marker = ManagedChildStartEvidence.create( + started_at=now.isoformat().replace("+00:00", "Z"), + dispatch_id=dispatch.dispatch_id, + dispatch_session_id=dispatch.dispatch_session_id, + run_id=dispatch.run_id, + managed_dispatch_binding_sha256=dispatch_binding_sha256, + authenticated_runner_id_sha256=hashlib.sha256( + dispatch.runner_id.encode("utf-8") + ).hexdigest(), + authenticated_session_id_sha256=hashlib.sha256( + dispatch.runner_session_id.encode("utf-8") + ).hexdigest(), + execution_authority_id=dispatch.execution_authority_id, + execution_authority_sha256=dispatch.execution_authority_sha256, + execution_authority_signer_sha256=( + dispatch.execution_authority_signer_sha256 + ), + run_store_identity_sha256=self._run_store_identity_sha256(run_dir), + ) + self._write_private_bytes_atomic_exclusive( + run_dir / "managed-child-started.json", canonical_json(marker) + ) + return marker + + def _read_child_start_evidence( + self, + *, + dispatch: HostedDispatch, + run_dir: Path, + dispatch_binding_sha256: str, + ) -> ManagedChildStartEvidence: + raw = self._read_private_bytes( + run_dir / "managed-child-started.json", + maximum_bytes=64 * 1024, + label="managed child start evidence", + ) + try: + marker = ManagedChildStartEvidence.model_validate_json(raw) + except ValueError as exc: + raise ValueError("managed child start evidence is invalid") from exc + expected = ( + dispatch.dispatch_id, + dispatch.dispatch_session_id, + dispatch.run_id, + dispatch_binding_sha256, + hashlib.sha256(dispatch.runner_id.encode("utf-8")).hexdigest(), + hashlib.sha256(dispatch.runner_session_id.encode("utf-8")).hexdigest(), + dispatch.execution_authority_id, + dispatch.execution_authority_sha256, + dispatch.execution_authority_signer_sha256, + self._run_store_identity_sha256(run_dir), + ) + actual = ( + marker.dispatch_id, + marker.dispatch_session_id, + marker.run_id, + marker.managed_dispatch_binding_sha256, + marker.authenticated_runner_id_sha256, + marker.authenticated_session_id_sha256, + marker.execution_authority_id, + marker.execution_authority_sha256, + marker.execution_authority_signer_sha256, + marker.run_store_identity_sha256, + ) + if canonical_json(marker) != raw or actual != expected: + raise ValueError("managed child start evidence changed") + return marker + + @staticmethod + def _managed_result_loss_report( + *, + dispatch: HostedDispatch, + workflow: Workflow, + manifest: object, + marker: ManagedChildStartEvidence, + evidence: ManagedResultLossEvidence, + verified_params: dict[str, RuntimeParamScalar], + runtime_substrate: Literal["web", "windows", "macos", "linux", "rdp", "citrix"], + ) -> RunReport: + authorization = getattr(manifest, "governed_authorization", None) + if ( + workflow.manifest is None + or authorization is None + or authorization.execution_profile + not in { + "standard", + "regulated", + } + ): + raise ValueError("managed result loss lacks retained authorization") + qualification_case_id_sha256 = ( + hashlib.sha256( + authorization.qualification_case_id.encode("utf-8") + ).hexdigest() + if authorization.qualification_case_id is not None + else None + ) + report = RunReport( + workflow_name=workflow.name, + started_at=marker.started_at, + execution_profile=authorization.execution_profile, + execution_outcome="HALTED", + production_eligible=False, + execution_completed=False, + transaction_outcome="RECONCILIATION_REQUIRED", + transaction_billable=False, + transaction_platform_fault=False, + managed_result_loss=evidence, + execution_target_kind=runtime_substrate, + recorded_surface=workflow.surface, + bundle_content_digest=workflow.manifest.content_digest, + workflow_contract_sha256=workflow_contract_sha256(workflow), + source_recording_sha256=workflow.manifest.provenance.source_recording_sha256, + parameter_schema_sha256=compute_parameter_schema_digest(workflow), + governed_authorization_id=authorization.authorization_id, + governed_approval_source=authorization.approval_source, + governed_authorization_created_at=authorization.created_at, + governed_policy_name=authorization.admitted_policy_name, + governed_policy_contract_sha256=( + authorization.admitted_policy_contract_sha256 + ), + governed_minimum_effect_tier=authorization.minimum_effect_tier, + governed_qualified_effect_requirements=list( + authorization.qualified_effect_requirements + ), + governed_runtime_inputs_digest=authorization.runtime_inputs_digest, + run_id_sha256=evidence.flow_run_id_sha256, + governed_qualification_project_id=authorization.qualification_project_id, + governed_qualification_project_revision=( + authorization.qualification_project_revision + ), + governed_qualification_project_contract_sha256=( + authorization.qualification_project_contract_sha256 + ), + governed_qualification_campaign_id_sha256=( + authorization.qualification_campaign_id_sha256 + ), + governed_qualification_case_id_sha256=qualification_case_id_sha256, + governed_qualification_case_input_sha256=( + authorization.qualification_case_input_sha256 + ), + governed_qualification_run_id_sha256=( + authorization.qualification_run_id_sha256 + ), + governed_qualification_case_kind=authorization.qualification_case_kind, + governed_qualification_case_action_paths=dict( + authorization.qualification_case_action_paths + ), + governed_qualification_fault_driver_id=( + authorization.qualification_fault_driver_id + ), + governed_qualification_fault_driver_contract_sha256=( + authorization.qualification_fault_driver_contract_sha256 + ), + governed_qualification_fault_driver_key_id=( + authorization.qualification_fault_driver_key_id + ), + governed_qualification_fault_step_id_sha256=( + authorization.qualification_fault_step_id_sha256 + ), + governed_authorized_effect_contracts={ + approval.step_id: list(approval.effect_contract_hashes) + for approval in authorization.unverified_write_approvals + }, + required_identity_step_ids=list(authorization.required_identity_step_ids), + approved_unverified_effect_step_ids=[ + approval.step_id + for approval in authorization.unverified_write_approvals + ], + params=verified_params, + results=[ + StepResult( + step_id="", + intent="retain managed child result loss", + ok=False, + safety_halt=True, + failure_category="safety_halt", + delivery_attempted=None, + ) + ], + success=False, + model_calls=int(getattr(manifest, "model_calls", 0)), + # The exact Cloud authority interaction proves that network I/O + # occurred even though the child report is unavailable. + external_network_calls="observed", + ) + stamp_execution_outcome( + report, + workflow, + authorization.execution_profile, + ) + if ( + report.execution_outcome != "HALTED" + or report.transaction_outcome != "RECONCILIATION_REQUIRED" + ): + raise ValueError("managed result loss classification changed") + return RunReport.model_validate_json(report.model_dump_json()) + + def _produce_managed_result_loss( + self, + *, + dispatch: HostedDispatch, + workflow: Workflow, + run_dir: Path, + qualification: ProductionQualificationAuthority, + private_key: Ed25519PrivateKey, + verified_params: dict[str, RuntimeParamScalar], + dispatch_binding_sha256: str, + closure_authority: HostedRunnerTransport, + loss_code: Literal[ + "runner_exception", + "report_missing", + "recovered_after_restart", + ], + ) -> tuple[ProductionTerminalVerificationEnvelope, str, RunReport]: + """Seal one result-loss report without resolving or replaying delivery.""" + + store = CheckpointStore(run_dir) + manifest = store.read_manifest() + if manifest is None: + raise ValueError("managed result loss lacks a retained run manifest") + marker = self._read_child_start_evidence( + dispatch=dispatch, + run_dir=run_dir, + dispatch_binding_sha256=dispatch_binding_sha256, + ) + + def snapshot_from_closure( + *, + request: ProductionDeliveryResultLossClosureRequest, + retained_loss_code: Literal[ + "runner_exception", + "report_missing", + "recovered_after_restart", + ], + closure_artifact: ProductionDeliveryResultLossClosureArtifact, + retained_chain: ProductionDeliveryPermitChain, + ) -> ManagedResultLossSnapshot: + chain = ProductionDeliveryPermitChain.model_validate( + retained_chain.model_dump(mode="json") + ) + if not chain.entries and chain.pending is None: + raise ValueError("managed result loss closure returned no permit") + closure_artifact = ( + ProductionDeliveryResultLossClosureArtifact.model_validate( + closure_artifact.model_dump(mode="json") + ) + ) + closure = closure_artifact.payload + if ( + closure.closure_request_sha256 != request.request_sha256() + or closure.result_loss_observed_at != request.result_loss_observed_at + or request.child_start_evidence != marker + ): + raise ValueError("managed result loss closure changed its request") + first = chain.entries[0] if chain.entries else chain.pending + assert first is not None + pending = chain.pending + evidence = ManagedResultLossEvidence.create( + loss_code=retained_loss_code, + child_started_at=marker.started_at, + child_start_evidence_sha256=marker.marker_sha256, + run_store_identity_sha256=marker.run_store_identity_sha256, + observed_at=request.result_loss_observed_at, + run_id=dispatch.run_id, + flow_run_id_sha256=hashlib.sha256( + dispatch.run_id.encode("utf-8") + ).hexdigest(), + dispatch_id=dispatch.dispatch_id, + dispatch_session_id=dispatch.dispatch_session_id, + managed_dispatch_binding_sha256=dispatch_binding_sha256, + idempotency_key_sha256=managed_result_loss_idempotency_sha256( + dispatch.idempotency_key + ), + authenticated_runner_id_sha256=hashlib.sha256( + dispatch.runner_id.encode("utf-8") + ).hexdigest(), + authenticated_session_id_sha256=hashlib.sha256( + dispatch.runner_session_id.encode("utf-8") + ).hexdigest(), + execution_authority_id=dispatch.execution_authority_id, + execution_authority_sha256=dispatch.execution_authority_sha256, + execution_authority_signer_sha256=( + dispatch.execution_authority_signer_sha256 + ), + delivery_result_loss_closure_artifact_sha256=( + closure_artifact.artifact_sha256() + ), + pending_permit_artifact_sha256=( + pending.permit_artifact_sha256 if pending is not None else None + ), + run_request_sha256=first.run_request_sha256, + pending_action_request_sha256=( + pending.action_request_sha256 if pending is not None else None + ), + ) + verify_production_delivery_result_loss_closure_binding( + closure_artifact, + permit_chain=chain, + result_loss=evidence, + tenant_id=dispatch.tenant_id, + terminal_verified_at=closure.closed_at, + ) + return ManagedResultLossSnapshot.create( + evidence=evidence, + permit_chain=chain, + closure_artifact=closure_artifact, + ) + + request_path = run_dir / "managed-result-loss-closure-request.json" + + def read_retained_request() -> ProductionDeliveryResultLossClosureRequest: + raw = self._read_private_bytes( + request_path, + maximum_bytes=64 * 1024, + label="managed result loss closure request", + ) + try: + retained = ( + ProductionDeliveryResultLossClosureRequest.model_validate_json(raw) + ) + except ValueError as exc: + raise ValueError( + "managed result loss closure request is invalid" + ) from exc + if ( + canonical_json(retained) != raw + or retained.child_start_evidence != marker + ): + raise ValueError("managed result loss closure request changed") + return retained + + try: + request_path.lstat() + except FileNotFoundError: + observed_at = ( + datetime.now(timezone.utc) + .replace(microsecond=0) + .isoformat() + .replace("+00:00", "Z") + ) + request = ProductionDeliveryResultLossClosureRequest( + child_start_evidence=marker, + result_loss_observed_at=observed_at, + ) + try: + self._write_private_bytes_atomic_exclusive( + request_path, + request.canonical_bytes(), + ) + except FileExistsError: + request = read_retained_request() + else: + request = read_retained_request() + + evidence_path = run_dir / "managed-result-loss.json" + + def read_retained_snapshot() -> ManagedResultLossSnapshot: + retained_bytes = self._read_private_bytes( + evidence_path, + maximum_bytes=_MAX_RESULT_LOSS_SNAPSHOT_BYTES, + label="managed result loss snapshot", + ) + try: + retained = ManagedResultLossSnapshot.model_validate_json(retained_bytes) + except ValueError as exc: + raise ValueError("managed result loss snapshot is invalid") from exc + if canonical_json(retained) != retained_bytes: + raise ValueError("managed result loss snapshot is not canonical") + expected_retained = snapshot_from_closure( + request=request, + retained_loss_code=retained.evidence.loss_code, + closure_artifact=retained.closure_artifact, + retained_chain=retained.permit_chain, + ) + if retained != expected_retained: + raise ValueError("managed result loss snapshot changed") + return retained + + try: + evidence_path.lstat() + except FileNotFoundError: + raw_result = closure_authority.close_result_loss( + dispatch.run_id, + dispatch.lease_token, + request, + ) + result = ProductionDeliveryResultLossClosureResult.model_validate( + raw_result + ) + closure_artifact, permit_chain = result.artifacts() + snapshot = snapshot_from_closure( + request=request, + retained_loss_code=loss_code, + closure_artifact=closure_artifact, + retained_chain=permit_chain, + ) + try: + self._write_private_bytes_atomic_exclusive( + evidence_path, canonical_json(snapshot) + ) + except FileExistsError: + snapshot = read_retained_snapshot() + else: + snapshot = read_retained_snapshot() + evidence = snapshot.evidence + observed_at = evidence.observed_at + expected = qualification.expected + report = self._managed_result_loss_report( + dispatch=dispatch, + workflow=workflow, + manifest=manifest, + marker=marker, + evidence=evidence, + verified_params=verified_params, + runtime_substrate=expected.runtime_build_identity.substrate, + ) + proof, digest = self._produce_terminal_verification( + dispatch=dispatch, + report=report, + run_dir=run_dir, + qualification=qualification, + private_key=private_key, + verified_params=verified_params, + dispatch_binding_sha256=dispatch_binding_sha256, + result_loss_observed_at=observed_at, + retained_permit_chain=snapshot.permit_chain, + retained_result_loss_closure=snapshot.closure_artifact, + ) + return proof, digest, report + + def _revalidate_terminal_authority( + self, + *, + dispatch: HostedDispatch, + runner_config: Path, + configured_origin: str, + initial_qualification: ProductionQualificationAuthority, + initial_deployment_bytes: bytes, + ) -> tuple[Ed25519PrivateKey, ProductionQualificationAuthority]: + """Revalidate all live signing authority after managed execution.""" + + terminal_config = load_runner_config(runner_config, protected=True) + if self._protected_runner_origin(terminal_config) != configured_origin: + raise ValueError("protected runner origin changed during execution") + self._verify_product_release(dispatch, terminal_config) + terminal_key = self._load_evidence_private_key(terminal_config) + terminal_qualification, terminal_deployment = self._verify_workflow_admission( + dispatch, + terminal_config, + evidence_private_key=terminal_key, + ) + if ( + terminal_qualification != initial_qualification + or terminal_deployment != initial_deployment_bytes + ): + raise ValueError("production admission changed during execution") + return terminal_key, terminal_qualification + def _resolve_params( self, dispatch: HostedDispatch, @@ -1126,6 +1858,11 @@ def _produce_terminal_verification( private_key: Ed25519PrivateKey, verified_params: dict[str, RuntimeParamScalar], dispatch_binding_sha256: str, + result_loss_observed_at: str | None = None, + retained_permit_chain: ProductionDeliveryPermitChain | None = None, + retained_result_loss_closure: ( + ProductionDeliveryResultLossClosureArtifact | None + ) = None, ) -> tuple[ProductionTerminalVerificationEnvelope, str]: """Build, retain, reread, and verify one exact terminal-v2 proof.""" @@ -1239,22 +1976,45 @@ def _produce_terminal_verification( prepared = prepare_production_terminal_evidence(report) now = datetime.now(timezone.utc).replace(microsecond=0) + if retained_result_loss_closure is not None: + closure_time = _utc_seconds( + retained_result_loss_closure.payload.closed_at, + label="managed result loss authority closure", + ) + now = max(now, closure_time) now_text = now.isoformat().replace("+00:00", "Z") - chain = DurableAuthority(run_dir, store).production_delivery_permit_chain( - allow_empty=( - prepared.transaction_outcome is TransactionOutcome.HALTED_BEFORE_EFFECT - ), - allow_pending=( - prepared.transaction_outcome - is TransactionOutcome.RECONCILIATION_REQUIRED - ), - receipt_absence_observed_at=( - now_text - if prepared.transaction_outcome - is TransactionOutcome.RECONCILIATION_REQUIRED - else None - ), + chain = ( + ProductionDeliveryPermitChain.model_validate( + retained_permit_chain.model_dump(mode="json") + ) + if retained_permit_chain is not None + else DurableAuthority(run_dir, store).production_delivery_permit_chain( + allow_empty=( + prepared.transaction_outcome + is TransactionOutcome.HALTED_BEFORE_EFFECT + ), + allow_pending=( + prepared.transaction_outcome + is TransactionOutcome.RECONCILIATION_REQUIRED + ), + receipt_absence_observed_at=( + now_text + if prepared.transaction_outcome + is TransactionOutcome.RECONCILIATION_REQUIRED + else None + ), + ) ) + if retained_permit_chain is not None and report.managed_result_loss is None: + raise ValueError("only managed result loss can use a retained permit chain") + if (report.managed_result_loss is not None) != all( + ( + result_loss_observed_at is not None, + retained_permit_chain is not None, + retained_result_loss_closure is not None, + ) + ): + raise ValueError("managed result loss observation binding is incomplete") first = chain.entries[0] if chain.entries else chain.pending expected = qualification.expected runtime = expected.runtime_build_identity @@ -1289,6 +2049,43 @@ def _produce_terminal_verification( != runner_session_id_sha256 ): raise ValueError("retained delivery chain differs from admitted live state") + if report.managed_result_loss is not None: + loss = report.managed_result_loss + pending = chain.pending + first_permit = chain.entries[0] if chain.entries else pending + pending_digest = ( + pending.permit_artifact_sha256 if pending is not None else None + ) + if ( + first_permit is None + or loss.observed_at != result_loss_observed_at + or loss.pending_permit_artifact_sha256 != pending_digest + or loss.run_request_sha256 != first_permit.run_request_sha256 + or loss.pending_action_request_sha256 + != (pending.action_request_sha256 if pending is not None else None) + or loss.dispatch_id != dispatch.dispatch_id + or loss.dispatch_session_id != dispatch.dispatch_session_id + or loss.managed_dispatch_binding_sha256 + != dispatch.payload.dispatch_binding_sha256 + or loss.idempotency_key_sha256 + != managed_result_loss_idempotency_sha256(dispatch.idempotency_key) + or loss.authenticated_runner_id_sha256 != runner_id_sha256 + or loss.authenticated_session_id_sha256 != runner_session_id_sha256 + or loss.execution_authority_id != dispatch.execution_authority_id + or loss.execution_authority_sha256 + != dispatch.execution_authority_sha256 + or loss.execution_authority_signer_sha256 + != dispatch.execution_authority_signer_sha256 + ): + raise ValueError("managed result loss differs from retained live state") + assert retained_result_loss_closure is not None + verify_production_delivery_result_loss_closure_binding( + retained_result_loss_closure, + permit_chain=chain, + result_loss=loss, + tenant_id=dispatch.tenant_id, + terminal_verified_at=now_text, + ) context = ProductionTerminalVerificationContext( run_id=dispatch.run_id, tenant_id=dispatch.tenant_id, @@ -1321,6 +2118,7 @@ def _produce_terminal_verification( dispatch.execution_authority_signer_sha256 ), permit_chain=chain, + delivery_result_loss_closure=retained_result_loss_closure, run_report_object_version="sha256:" + prepared.report_sha256, verified_at=now_text, issued_at=now_text, @@ -1336,6 +2134,8 @@ def _produce_terminal_verification( ): raise ValueError("terminal report changed during proof production") envelope_bytes = canonical_json(built.envelope) + if len(envelope_bytes) > _MAX_ARTIFACT_BYTES: + raise ValueError("terminal verification exceeds the callback size limit") payload = built.envelope.payload final = chain.entries[-1] if chain.entries else None final_authority = chain.pending or final @@ -1399,6 +2199,8 @@ def _produce_terminal_verification( run_report_object_version=context.run_report_object_version, run_report_object_sha256=built.report_sha256, evidence_manifests=payload.evidence_manifests, + managed_result_loss=payload.managed_result_loss, + delivery_result_loss_closure=payload.delivery_result_loss_closure, ) artifact_sha256 = verify_production_terminal_verification_from_report( built.envelope, @@ -1409,50 +2211,145 @@ def _produce_terminal_verification( if artifact_sha256 != hashlib.sha256(envelope_bytes).hexdigest(): raise ValueError("terminal verification artifact digest changed") - # Final-named evidence exists only after the complete in-memory proof - # passes. If storage or the required reread fails, remove only files - # created by this call so no failed terminalization leaves success - # artifacts behind. + # One exact CAS object owns the terminal state. Once it exists, a late + # child result or a concurrent terminalizer can only return the same + # bytes; it can never replace reconciliation with another outcome. report_path = run_dir / "production-terminal-report.json" envelope_path = run_dir / "production-terminal-verification.json" - written: list[Path] = [] + state_path = run_dir / "production-terminal-state.json" + state_bytes = canonical_json( + { + "schema_version": "openadapt.production-terminal-state/v1", + "report_bytes_base64": b64encode(built.report_bytes).decode("ascii"), + "report_sha256": built.report_sha256, + "terminal_verification_bytes_base64": b64encode(envelope_bytes).decode( + "ascii" + ), + "terminal_verification_sha256": artifact_sha256, + } + ) try: - self._write_private_bytes(report_path, built.report_bytes) - written.append(report_path) - self._write_private_bytes(envelope_path, envelope_bytes) - written.append(envelope_path) - stored_report = self._read_private_bytes( - report_path, - maximum_bytes=_MAX_ARTIFACT_BYTES, - label="production terminal report", - ) - stored_envelope = self._read_private_bytes( - envelope_path, - maximum_bytes=_MAX_ARTIFACT_BYTES, - label="production terminal verification", - ) - if stored_report != built.report_bytes or stored_envelope != envelope_bytes: - raise ValueError("stored terminal evidence changed after write") - reread = ProductionTerminalVerificationEnvelope.model_validate_json( - stored_envelope + self._write_private_bytes_atomic_exclusive(state_path, state_bytes) + except FileExistsError: + retained_state = self._read_private_bytes( + state_path, + maximum_bytes=_MAX_TERMINAL_STATE_BYTES, + label="production terminal state", ) - if canonical_json(reread) != stored_envelope: - raise ValueError("stored terminal verification is not canonical") - reread_sha256 = verify_production_terminal_verification_from_report( - reread, - report_bytes=stored_report, - expected=live_expected, - now=now, - ) - if reread_sha256 != artifact_sha256: - raise ValueError("stored terminal verification digest changed") - except Exception: - for path in reversed(written): + if retained_state != state_bytes: try: - path.unlink() - except OSError: - pass - raise + retained_value = json.loads(retained_state.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ValueError("retained terminal state is invalid") from exc + expected_state_keys = { + "schema_version", + "report_bytes_base64", + "report_sha256", + "terminal_verification_bytes_base64", + "terminal_verification_sha256", + } + if ( + not isinstance(retained_value, dict) + or set(retained_value) != expected_state_keys + or retained_value["schema_version"] + != "openadapt.production-terminal-state/v1" + or not all( + isinstance(retained_value[field], str) + for field in expected_state_keys - {"schema_version"} + ) + or canonical_json(retained_value) != retained_state + ): + raise ValueError("retained terminal state is invalid") + try: + retained_report = b64decode( + retained_value["report_bytes_base64"], validate=True + ) + retained_envelope = b64decode( + retained_value["terminal_verification_bytes_base64"], + validate=True, + ) + except (TypeError, ValueError) as exc: + raise ValueError("retained terminal state is invalid") from exc + if ( + len(retained_report) > _MAX_ARTIFACT_BYTES + or len(retained_envelope) > _MAX_ARTIFACT_BYTES + or retained_report != built.report_bytes + or hashlib.sha256(retained_report).hexdigest() + != retained_value["report_sha256"] + or hashlib.sha256(retained_envelope).hexdigest() + != retained_value["terminal_verification_sha256"] + ): + raise ValueError("a different terminal outcome is already sealed") + try: + retained_proof = ( + ProductionTerminalVerificationEnvelope.model_validate_json( + retained_envelope + ) + ) + except ValueError as exc: + raise ValueError("retained terminal state is invalid") from exc + retained_sha256 = verify_production_terminal_verification_from_report( + retained_proof, + report_bytes=retained_report, + expected=live_expected, + now=now, + ) + if ( + canonical_json(retained_proof) != retained_envelope + or retained_sha256 != retained_value["terminal_verification_sha256"] + ): + raise ValueError("retained terminal state is invalid") + state_bytes = retained_state + envelope_bytes = retained_envelope + artifact_sha256 = retained_sha256 + for path, raw, label in ( + (report_path, built.report_bytes, "production terminal report"), + (envelope_path, envelope_bytes, "production terminal verification"), + ): + try: + self._write_private_bytes_atomic_exclusive(path, raw) + except FileExistsError: + retained = self._read_private_bytes( + path, + maximum_bytes=_MAX_ARTIFACT_BYTES, + label=label, + ) + if retained != raw: + raise ValueError("a different terminal artifact is already sealed") + stored_report = self._read_private_bytes( + report_path, + maximum_bytes=_MAX_ARTIFACT_BYTES, + label="production terminal report", + ) + stored_envelope = self._read_private_bytes( + envelope_path, + maximum_bytes=_MAX_ARTIFACT_BYTES, + label="production terminal verification", + ) + if stored_report != built.report_bytes or stored_envelope != envelope_bytes: + raise ValueError("stored terminal evidence changed after write") + reread = ProductionTerminalVerificationEnvelope.model_validate_json( + stored_envelope + ) + if canonical_json(reread) != stored_envelope: + raise ValueError("stored terminal verification is not canonical") + reread_sha256 = verify_production_terminal_verification_from_report( + reread, + report_bytes=stored_report, + expected=live_expected, + now=now, + ) + if reread_sha256 != artifact_sha256: + raise ValueError("stored terminal verification digest changed") + if ( + self._read_private_bytes( + state_path, + maximum_bytes=_MAX_TERMINAL_STATE_BYTES, + label="production terminal state", + ) + != state_bytes + ): + raise ValueError("stored terminal state changed") return reread, built.report_sha256 @staticmethod @@ -1519,6 +2416,113 @@ def reconciliation_required( raise ValueError("reconciliation code is invalid") raise RuntimeError("managed run requires signed terminal reconciliation") + def recover_uncertain_run( + self, + dispatch: HostedDispatch | Mapping[str, object], + *, + runner_config: Path, + run_dir: Path, + closure_authority: HostedRunnerTransport, + ) -> HostedRunResult: + """Return or create the one signed terminal for a lost managed result. + + This path never invokes the managed child. It accepts only the exact + original dispatch, protected run store, live admissions, child-start + marker, and unresolved terminal permit. + """ + + parsed = HostedDispatch.model_validate(dispatch) + config = load_runner_config(runner_config, protected=True) + self._protected_runner_origin(config) + self._verify_product_release(parsed, config) + key = self._load_evidence_private_key(config) + qualification, deployment_bytes = self._verify_workflow_admission( + parsed, + config, + evidence_private_key=key, + ) + params = self._resolve_params(parsed, config) + run_dir = Path(run_dir) + run_stat = run_dir.lstat() + if ( + not stat.S_ISDIR(run_stat.st_mode) + or stat.S_ISLNK(run_stat.st_mode) + or ( + os.name != "nt" + and ( + run_stat.st_uid != os.geteuid() + or stat.S_IMODE(run_stat.st_mode) != 0o700 + ) + ) + ): + raise ValueError("hosted recovery run directory is not protected") + profile_path = run_dir / "deployment.yaml" + if ( + self._read_private_bytes( + profile_path, + maximum_bytes=1024 * 1024, + label="staged hosted deployment profile", + ) + != deployment_bytes + ): + raise ValueError("staged hosted deployment profile changed") + staged_profiles = dict(config.profiles) + staged_profiles[parsed.payload.deployment_profile_id] = profile_path + verified = verify_dispatch( + parsed.payload, + replace(config, profiles=staged_profiles), + resolved_params=params, + ) + if isinstance(verified, Refusal): + raise ValueError("hosted recovery dispatch no longer verifies") + self._read_child_start_evidence( + dispatch=parsed, + run_dir=run_dir, + dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, + ) + reservation_key = f"{parsed.tenant_id}:{parsed.idempotency_key}" + reservation = self._ledger.lookup(reservation_key) + if reservation is None or reservation.get("run_id") != parsed.run_id: + raise ValueError("hosted recovery lacks the original reservation") + + proof, report_digest, report = self._produce_managed_result_loss( + dispatch=parsed, + workflow=verified.workflow, + run_dir=run_dir, + qualification=qualification, + private_key=key, + verified_params=params, + dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, + closure_authority=closure_authority, + loss_code="recovered_after_restart", + ) + outcome = TransactionOutcome.RECONCILIATION_REQUIRED + self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) + events = tuple( + report_events( + report, + run_id=parsed.run_id, + workflow_id=parsed.workflow_id, + bundle_digest=parsed.payload.bundle.content_digest, + authorization_id=parsed.payload.authorization.authorization_id, + consequential_steps=verified.consequential_steps, + effect_covered_consequential_steps=( + verified.effect_covered_consequential_steps + ), + terminal_outcome=outcome.value, + ) + ) + return HostedRunResult( + dispatch_id=parsed.dispatch_id, + run_id=parsed.run_id, + outcome=outcome, + evidence_batch=events, + terminal_verification=proof, + started=True, + uncertain_delivery=proof.payload.pending_permit_count == 1, + report_sha256=report_digest, + ) + def execute( self, dispatch: HostedDispatch | Mapping[str, object], @@ -1526,10 +2530,12 @@ def execute( runner_config: Path, run_dir: Path, authority: DeliveryAuthority, + closure_authority: HostedRunnerTransport | None = None, ) -> Union[HostedRunResult, HostedDispatchRefusal]: parsed: HostedDispatch | None = None try: parsed = HostedDispatch.model_validate(dispatch) + assert parsed is not None expiry = _utc_seconds(parsed.lease_expires_at, label="hosted lease expiry") if datetime.now(timezone.utc) >= expiry: raise ValueError("hosted lease expired before execution") @@ -1600,6 +2606,7 @@ def execute( f"prestart_{type(exc).__name__}", ) + assert parsed is not None reservation_key = f"{parsed.tenant_id}:{parsed.idempotency_key}" try: self._ledger.reserve(reservation_key, run_id=parsed.run_id) @@ -1664,26 +2671,165 @@ def execute( report_sha256="0" * 64, ) + try: + self._write_child_start_evidence( + dispatch=parsed, + run_dir=run_dir, + dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, + ) + except Exception: + outcome = TransactionOutcome.FAILED_PLATFORM + self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) + events = tuple( + failure_events( + run_id=parsed.run_id, + bundle_digest=parsed.payload.bundle.content_digest, + authorization_id=parsed.payload.authorization.authorization_id, + ) + ) + return HostedRunResult( + dispatch_id=parsed.dispatch_id, + run_id=parsed.run_id, + outcome=outcome, + evidence_batch=events, + started=False, + uncertain_delivery=False, + report_sha256="0" * 64, + ) + try: # Once this call starts, the child can reach a real input edge. Any # lost or malformed result is uncertain and can never be retried. execution = self._runner(argv, run_dir, child_env) - except Exception as exc: + except Exception: outcome = TransactionOutcome.RECONCILIATION_REQUIRED + try: + if closure_authority is None: + raise ValueError("hosted result-loss closure authority is absent") + terminal_key, terminal_qualification = ( + self._revalidate_terminal_authority( + dispatch=parsed, + runner_config=runner_config, + configured_origin=configured_origin, + initial_qualification=qualification, + initial_deployment_bytes=deployment_bytes, + ) + ) + exception_loss_proof, report_digest, exception_loss_report = ( + self._produce_managed_result_loss( + dispatch=parsed, + workflow=verified.workflow, + run_dir=run_dir, + qualification=terminal_qualification, + private_key=terminal_key, + verified_params=params, + dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, + closure_authority=closure_authority, + loss_code="runner_exception", + ) + ) + except Exception as terminal_exc: + self._ledger.record_outcome( + reservation_key, outcome, run_id=parsed.run_id + ) + raise RuntimeError( + "managed run requires signed terminal reconciliation" + ) from terminal_exc self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) - raise RuntimeError( - "managed run requires signed terminal reconciliation" - ) from exc + events = tuple( + report_events( + exception_loss_report, + run_id=parsed.run_id, + workflow_id=parsed.workflow_id, + bundle_digest=parsed.payload.bundle.content_digest, + authorization_id=parsed.payload.authorization.authorization_id, + consequential_steps=verified.consequential_steps, + effect_covered_consequential_steps=( + verified.effect_covered_consequential_steps + ), + terminal_outcome=outcome.value, + ) + ) + return HostedRunResult( + dispatch_id=parsed.dispatch_id, + run_id=parsed.run_id, + outcome=outcome, + evidence_batch=events, + terminal_verification=exception_loss_proof, + started=True, + uncertain_delivery=( + exception_loss_proof.payload.pending_permit_count == 1 + ), + report_sha256=report_digest, + ) if execution.report_bytes is None: outcome = TransactionOutcome.RECONCILIATION_REQUIRED + try: + if closure_authority is None: + raise ValueError("hosted result-loss closure authority is absent") + terminal_key, terminal_qualification = ( + self._revalidate_terminal_authority( + dispatch=parsed, + runner_config=runner_config, + configured_origin=configured_origin, + initial_qualification=qualification, + initial_deployment_bytes=deployment_bytes, + ) + ) + missing_loss_proof, report_digest, missing_loss_report = ( + self._produce_managed_result_loss( + dispatch=parsed, + workflow=verified.workflow, + run_dir=run_dir, + qualification=terminal_qualification, + private_key=terminal_key, + verified_params=params, + dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, + closure_authority=closure_authority, + loss_code="report_missing", + ) + ) + except Exception as terminal_exc: + self._ledger.record_outcome( + reservation_key, outcome, run_id=parsed.run_id + ) + raise RuntimeError( + "managed run requires signed terminal reconciliation" + ) from terminal_exc self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) - raise RuntimeError("managed run requires signed terminal reconciliation") + events = tuple( + report_events( + missing_loss_report, + run_id=parsed.run_id, + workflow_id=parsed.workflow_id, + bundle_digest=parsed.payload.bundle.content_digest, + authorization_id=parsed.payload.authorization.authorization_id, + consequential_steps=verified.consequential_steps, + effect_covered_consequential_steps=( + verified.effect_covered_consequential_steps + ), + terminal_outcome=outcome.value, + ) + ) + return HostedRunResult( + dispatch_id=parsed.dispatch_id, + run_id=parsed.run_id, + outcome=outcome, + evidence_batch=events, + terminal_verification=missing_loss_proof, + started=True, + uncertain_delivery=( + missing_loss_proof.payload.pending_permit_count == 1 + ), + report_sha256=report_digest, + ) report: RunReport | None = None report_digest = hashlib.sha256(execution.report_bytes).hexdigest() terminalization_error: Exception | None try: report = RunReport.model_validate_json(execution.report_bytes) + assert report is not None outcome = classify_transaction_outcome(report) proof: ProductionTerminalVerificationEnvelope | None = None if execution.terminal_verification is not None: @@ -1695,28 +2841,20 @@ def execute( }: if outcome is TransactionOutcome.VERIFIED and execution.returncode != 0: raise ValueError("managed child exited unsuccessfully") - terminal_config = load_runner_config(runner_config, protected=True) - if self._protected_runner_origin(terminal_config) != configured_origin: - raise ValueError("protected runner origin changed during execution") - self._verify_product_release(parsed, terminal_config) - terminal_key = self._load_evidence_private_key(terminal_config) - terminal_qualification, terminal_deployment = ( - self._verify_workflow_admission( - parsed, - terminal_config, - evidence_private_key=terminal_key, + terminal_key, terminal_qualification = ( + self._revalidate_terminal_authority( + dispatch=parsed, + runner_config=runner_config, + configured_origin=configured_origin, + initial_qualification=qualification, + initial_deployment_bytes=deployment_bytes, ) ) - if ( - terminal_qualification != qualification - or terminal_deployment != deployment_bytes - ): - raise ValueError("production admission changed during execution") proof, report_digest = self._produce_terminal_verification( dispatch=parsed, report=report, run_dir=run_dir, - qualification=qualification, + qualification=terminal_qualification, private_key=terminal_key, verified_params=params, dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, @@ -1763,7 +2901,7 @@ def execute( terminal_outcome=outcome.value, ) ) - uncertain = outcome is TransactionOutcome.RECONCILIATION_REQUIRED + uncertain = bool(proof is not None and proof.payload.pending_permit_count == 1) return HostedRunResult( dispatch_id=parsed.dispatch_id, run_id=parsed.run_id, diff --git a/openadapt_flow/terminal_verification_v2.py b/openadapt_flow/terminal_verification_v2.py index ec6d86c2..6d531024 100644 --- a/openadapt_flow/terminal_verification_v2.py +++ b/openadapt_flow/terminal_verification_v2.py @@ -34,6 +34,7 @@ from openadapt_flow.ir import ( EffectVerificationEvidence, + ManagedResultLossEvidence, PostconditionContractEvidence, RunReport, ) @@ -96,6 +97,18 @@ EXECUTION_AUTHORITY_SCHEMA: Final[ Literal["openadapt.production-execution-authority/v2"] ] = "openadapt.production-execution-authority/v2" +RESULT_LOSS_CLOSURE_REQUEST_DOMAIN: Final[bytes] = ( + b"openadapt.production-delivery-result-loss-closure-request.v2\0" +) +RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN: Final[bytes] = ( + b"openadapt.production-delivery-result-loss-closure-payload.v2\0" +) +RESULT_LOSS_CLOSURE_PAYLOAD_SCHEMA: Final[ + Literal["openadapt.production-delivery-result-loss-closure-payload/v2"] +] = "openadapt.production-delivery-result-loss-closure-payload/v2" +RESULT_LOSS_CLOSURE_ARTIFACT_SCHEMA: Final[ + Literal["openadapt.production-delivery-result-loss-closure-artifact/v2"] +] = "openadapt.production-delivery-result-loss-closure-artifact/v2" _SHA256_RE = re.compile(r"^[a-f0-9]{64}$") _UUID_RE = re.compile( @@ -178,7 +191,7 @@ class ProductionExecutionOutcome(ClosedSignedModel): passed_contracts: TerminalContractCounts workflow_contract_sha256: str = Field(pattern=_SHA256_RE) postcondition_evidence: tuple[PostconditionContractEvidence, ...] = Field( - min_length=1, max_length=10_000 + max_length=10_000 ) evidence_classes: tuple[ Literal[ @@ -195,9 +208,15 @@ class ProductionExecutionOutcome(ClosedSignedModel): model_calls: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) external_network_calls: Literal["none", "observed"] compensation_actions: StrictInt = Field(default=0, ge=0, le=JS_MAX_SAFE_INTEGER) + managed_result_loss_evidence_sha256: str | None = Field( + default=None, + pattern=_SHA256_RE, + exclude_if=lambda value: value is None, + ) @model_validator(mode="after") def _complete_terminal_outcome(self) -> "ProductionExecutionOutcome": + result_loss = self.managed_result_loss_evidence_sha256 is not None required = self.required_contracts.model_dump(mode="python") passed = self.passed_contracts.model_dump(mode="python") if any(passed[key] > required[key] for key in required): @@ -206,6 +225,8 @@ def _complete_terminal_outcome(self) -> "ProductionExecutionOutcome": raise ValueError("terminal outcome requires one authorization") if self.passed_contracts.authorization != 1: raise ValueError("terminal outcome requires one passed authorization") + if not result_loss and not self.postcondition_evidence: + raise ValueError("ordinary terminal outcome requires a postcondition") if len(self.postcondition_evidence) != self.required_contracts.postcondition: raise ValueError("terminal postcondition evidence cardinality is invalid") if ( @@ -276,6 +297,22 @@ def _complete_terminal_outcome(self) -> "ProductionExecutionOutcome": raise ValueError("only a VERIFIED terminal outcome is production eligible") elif self.outcome == "HALTED" and self.execution_completed: raise ValueError("terminal HALTED outcome cannot claim completed execution") + if result_loss and ( + self.outcome != "HALTED" + or self.production_eligible + or self.execution_completed + or self.required_contracts + != TerminalContractCounts( + authorization=1, + identity=0, + postcondition=0, + effect=0, + ) + or self.passed_contracts != self.required_contracts + or self.postcondition_evidence + or self.evidence_classes != ("authorization",) + ): + raise ValueError("managed result loss execution outcome is invalid") return self def artifact_sha256(self) -> str: @@ -340,9 +377,9 @@ class ProductionRunReceipt(ClosedSignedModel): ] authorization_required: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) authorization_confirmed: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) - identity_required: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) + identity_required: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) identity_confirmed: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) - postconditions_required: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) + postconditions_required: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) postconditions_confirmed: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) effects_required: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) effects_confirmed: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) @@ -359,9 +396,15 @@ class ProductionRunReceipt(ClosedSignedModel): source_receipt_digest: str = Field(pattern=_SHA256_RE) source_receipt_sha256: str = Field(pattern=_SHA256_RE) generated_at: str = Field(pattern=r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:00:00Z$") + managed_result_loss_evidence_sha256: str | None = Field( + default=None, + pattern=_SHA256_RE, + exclude_if=lambda value: value is None, + ) @model_validator(mode="after") def _closed_receipt(self) -> "ProductionRunReceipt": + result_loss = self.managed_result_loss_evidence_sha256 is not None for label, required, confirmed in ( ( "authorization", @@ -382,6 +425,12 @@ def _closed_receipt(self) -> "ProductionRunReceipt": raise ValueError("terminal receipt requires one authorization") if self.authorization_confirmed != 1: raise ValueError("terminal receipt requires one passed authorization") + if not result_loss and ( + self.identity_required < 1 or self.postconditions_required < 1 + ): + raise ValueError( + "ordinary terminal receipt requires identity and postcondition contracts" + ) if self.steps_ok > self.steps_total: raise ValueError("terminal receipt successful step count is invalid") if self.evidence_classes != tuple(sorted(self.evidence_classes)) or len( @@ -454,6 +503,23 @@ def _closed_receipt(self) -> "ProductionRunReceipt": or self.effect_tier_reached != "none" ): raise ValueError("HALTED terminal receipt does not prove no effect") + if result_loss and ( + self.transaction_outcome != "RECONCILIATION_REQUIRED" + or self.outcome != "HALTED" + or self.production_eligible + or self.source_schema_version != "openadapt.run-report/v1" + or self.steps_total != 1 + or self.steps_ok != 0 + or self.identity_required != 0 + or self.identity_confirmed != 0 + or self.postconditions_required != 0 + or self.postconditions_confirmed != 0 + or self.effects_required != 0 + or self.effects_confirmed != 0 + or self.evidence_classes != ("authorization",) + or self.effect_tier_reached != "none" + ): + raise ValueError("managed result loss receipt is invalid") return self @@ -621,6 +687,11 @@ def project_production_terminal_run_receipt( "source_receipt_digest": report_sha256, "source_receipt_sha256": report_sha256, "generated_at": _hour_utc(report.started_at), + "managed_result_loss_evidence_sha256": ( + report.managed_result_loss.evidence_sha256 + if report.managed_result_loss is not None + else None + ), } ) except (TypeError, ValueError) as exc: @@ -707,6 +778,11 @@ def prepare_production_terminal_evidence( "model_calls": envelope.model_calls, "external_network_calls": envelope.external_network_calls, "compensation_actions": envelope.compensation_actions, + "managed_result_loss_evidence_sha256": ( + validated.managed_result_loss.evidence_sha256 + if validated.managed_result_loss is not None + else None + ), } ) except (ReceiptError, ValueError) as exc: @@ -1309,6 +1385,149 @@ def artifact_sha256(self) -> str: return hashlib.sha256(self.canonical_bytes()).hexdigest() +class ProductionDeliveryResultLossClosurePayload(ClosedSignedModel): + """Cloud authority fence for one managed child result loss.""" + + schema_version: Literal[ + "openadapt.production-delivery-result-loss-closure-payload/v2" + ] = RESULT_LOSS_CLOSURE_PAYLOAD_SCHEMA + closure_id: str = Field(pattern=_UUID_RE) + closure_sequence: Literal[1] = 1 + closure_request_sha256: str = Field(pattern=_SHA256_RE) + closed_at: str + result_loss_observed_at: str + receipt_absence_observed_at: str | None = None + tenant_id: str = Field(pattern=_UUID_RE) + run_id: str = Field(pattern=_UUID_RE) + flow_run_id_sha256: str = Field(pattern=_SHA256_RE) + dispatch_id: str = Field(pattern=_UUID_RE) + dispatch_session_id: str = Field(pattern=_UUID_RE) + managed_dispatch_binding_sha256: str = Field(pattern=r"^sha256:[a-f0-9]{64}$") + idempotency_key_sha256: str = Field(pattern=_SHA256_RE) + authenticated_runner_id_sha256: str = Field(pattern=_SHA256_RE) + authenticated_session_id_sha256: str = Field(pattern=_SHA256_RE) + execution_authority_id: str = Field(pattern=_UUID_RE) + execution_authority_sha256: str = Field(pattern=_SHA256_RE) + execution_authority_signer_sha256: str = Field(pattern=_SHA256_RE) + child_started_at: str + child_start_evidence_sha256: str = Field(pattern=_SHA256_RE) + run_store_identity_sha256: str = Field(pattern=_SHA256_RE) + permit_chain_sha256: str = Field(pattern=_SHA256_RE) + permit_count: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) + acknowledged_permit_count: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + pending_permit_count: StrictInt = Field(ge=0, le=1) + pending_permit_artifact_sha256: str | None = Field( + default=None, + pattern=_SHA256_RE, + ) + run_request_sha256: str = Field(pattern=_SHA256_RE) + pending_action_request_sha256: str | None = Field( + default=None, + pattern=_SHA256_RE, + ) + final_input_edge_sequence: StrictInt = Field(ge=1, le=JS_MAX_SAFE_INTEGER) + final_authority_sequence: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + final_runtime_delivery_sequence: StrictInt = Field(ge=0, le=JS_MAX_SAFE_INTEGER) + delivery_state: Literal["CLOSED_UNRESOLVED_RESULT_LOSS"] = ( + "CLOSED_UNRESOLVED_RESULT_LOSS" + ) + effect_absence_claimed: Literal[False] = False + not_received_claimed: Literal[False] = False + blind_retry_authorized: Literal[False] = False + actuation_replay_authorized: Literal[False] = False + new_permit_authorized: Literal[False] = False + delivery_acknowledgment_authorized: Literal[False] = False + terminal_callback_required: Literal[True] = True + + @model_validator(mode="after") + def _closed_result_loss_fence(self) -> "ProductionDeliveryResultLossClosurePayload": + if hashlib.sha256(self.run_id.encode("utf-8")).hexdigest() != ( + self.flow_run_id_sha256 + ): + raise ValueError("result-loss closure run identity digest is invalid") + if self.permit_count != ( + self.acknowledged_permit_count + self.pending_permit_count + ): + raise ValueError("result-loss closure permit counts are inconsistent") + has_pending = self.pending_permit_count == 1 + if has_pending != (self.pending_permit_artifact_sha256 is not None): + raise ValueError("result-loss closure pending permit binding is incomplete") + if has_pending != (self.pending_action_request_sha256 is not None): + raise ValueError("result-loss closure pending action binding is incomplete") + if has_pending != (self.receipt_absence_observed_at is not None): + raise ValueError( + "result-loss closure receipt-absence binding is incomplete" + ) + child_started = _parse_utc( + self.child_started_at, + field="result-loss closure child_started_at", + ) + observed = _parse_utc( + self.result_loss_observed_at, + field="result-loss closure result_loss_observed_at", + ) + closed = _parse_utc(self.closed_at, field="result-loss closure closed_at") + if not child_started <= observed <= closed: + raise ValueError("result-loss closure chronology is invalid") + if self.receipt_absence_observed_at is not None: + absence = _parse_utc( + self.receipt_absence_observed_at, + field="result-loss closure receipt_absence_observed_at", + ) + if absence != observed: + raise ValueError( + "result-loss closure receipt absence differs from observation" + ) + return self + + def canonical_bytes(self) -> bytes: + return canonical_json(self) + + def payload_sha256(self) -> str: + return hashlib.sha256( + RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN + self.canonical_bytes() + ).hexdigest() + + +class ProductionDeliveryResultLossClosureArtifact(ClosedSignedModel): + schema_version: Literal[ + "openadapt.production-delivery-result-loss-closure-artifact/v2" + ] = RESULT_LOSS_CLOSURE_ARTIFACT_SCHEMA + payload: ProductionDeliveryResultLossClosurePayload + payload_sha256: str = Field(pattern=_SHA256_RE) + signer: DeliveryAuthoritySigner + signature: str = Field(min_length=86, max_length=86) + + @field_validator("signature") + @classmethod + def _canonical_signature(cls, value: str) -> str: + _decode_ed25519_signature(value, field="delivery result-loss closure") + return value + + @model_validator(mode="after") + def _verify_artifact(self) -> "ProductionDeliveryResultLossClosureArtifact": + payload = ProductionDeliveryResultLossClosurePayload.model_validate( + self.payload.model_dump(mode="json") + ) + if self.payload_sha256 != payload.payload_sha256(): + raise ValueError("delivery result-loss closure payload digest is invalid") + if self.signer.signer_sha256() != payload.execution_authority_signer_sha256: + raise ValueError("delivery result-loss closure signer is invalid") + _verify_authority_signature( + self.signer, + self.signature, + RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN + payload.canonical_bytes(), + field="delivery result-loss closure", + ) + return self + + def canonical_bytes(self) -> bytes: + return canonical_json(self) + + def artifact_sha256(self) -> str: + return hashlib.sha256(self.canonical_bytes()).hexdigest() + + def _decode_ed25519_signature(value: str, *, field: str) -> bytes: try: decoded = urlsafe_b64decode(value + "==") @@ -1382,6 +1601,26 @@ def sign_production_delivery_receipt( ) +def sign_production_delivery_result_loss_closure( + payload: ProductionDeliveryResultLossClosurePayload, + private_key: Ed25519PrivateKey, +) -> ProductionDeliveryResultLossClosureArtifact: + """Sign one monotonic result-loss closure with the delivery authority.""" + + payload = ProductionDeliveryResultLossClosurePayload.model_validate( + payload.model_dump(mode="json") + ) + signature = private_key.sign( + RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN + payload.canonical_bytes() + ) + return ProductionDeliveryResultLossClosureArtifact( + payload=payload, + payload_sha256=payload.payload_sha256(), + signer=_authority_signer(private_key), + signature=urlsafe_b64encode(signature).decode("ascii").rstrip("="), + ) + + class ProductionDeliveryPermit(ClosedSignedModel): """One acknowledged input edge in the terminal permit chain.""" @@ -1819,6 +2058,153 @@ def build( return cls(entries=entries, pending=pending, permit_chain_sha256=digest) +def verify_production_delivery_result_loss_closure_binding( + artifact: ProductionDeliveryResultLossClosureArtifact, + *, + permit_chain: ProductionDeliveryPermitChain, + result_loss: ManagedResultLossEvidence, + tenant_id: str, + terminal_verified_at: str, +) -> str: + """Verify one signed authority fence against the exact terminal snapshot.""" + + artifact = ProductionDeliveryResultLossClosureArtifact.model_validate( + artifact.model_dump(mode="json") + ) + permit_chain = ProductionDeliveryPermitChain.model_validate( + permit_chain.model_dump(mode="json") + ) + if not permit_chain.entries and permit_chain.pending is None: + raise ValueError("result-loss closure requires at least one permit") + payload = artifact.payload + all_permits: tuple[ + ProductionDeliveryPermit | ProductionPendingDeliveryPermit, ... + ] = ( + *permit_chain.entries, + *((permit_chain.pending,) if permit_chain.pending is not None else ()), + ) + first = all_permits[0] + final = all_permits[-1] + final_acknowledged = permit_chain.entries[-1] if permit_chain.entries else None + pending = permit_chain.pending + expected_pending_digest = ( + pending.permit_artifact_sha256 if pending is not None else None + ) + expected_pending_action = pending.action_request_sha256 if pending else None + expected_receipt_absence = ( + pending.receipt_absence_observed_at if pending is not None else None + ) + if ( + payload.tenant_id, + payload.run_id, + payload.flow_run_id_sha256, + payload.dispatch_id, + payload.dispatch_session_id, + payload.managed_dispatch_binding_sha256, + payload.idempotency_key_sha256, + payload.authenticated_runner_id_sha256, + payload.authenticated_session_id_sha256, + payload.execution_authority_id, + payload.execution_authority_sha256, + payload.execution_authority_signer_sha256, + payload.child_started_at, + payload.child_start_evidence_sha256, + payload.run_store_identity_sha256, + ) != ( + tenant_id, + result_loss.run_id, + result_loss.flow_run_id_sha256, + result_loss.dispatch_id, + result_loss.dispatch_session_id, + result_loss.managed_dispatch_binding_sha256, + result_loss.idempotency_key_sha256, + result_loss.authenticated_runner_id_sha256, + result_loss.authenticated_session_id_sha256, + result_loss.execution_authority_id, + result_loss.execution_authority_sha256, + result_loss.execution_authority_signer_sha256, + result_loss.child_started_at, + result_loss.child_start_evidence_sha256, + result_loss.run_store_identity_sha256, + ): + raise ValueError("result-loss closure identity binding is invalid") + if ( + payload.permit_chain_sha256, + payload.permit_count, + payload.acknowledged_permit_count, + payload.pending_permit_count, + payload.pending_permit_artifact_sha256, + payload.run_request_sha256, + payload.pending_action_request_sha256, + payload.final_input_edge_sequence, + payload.final_authority_sequence, + payload.final_runtime_delivery_sequence, + payload.receipt_absence_observed_at, + ) != ( + permit_chain.permit_chain_sha256, + len(all_permits), + len(permit_chain.entries), + 1 if pending is not None else 0, + expected_pending_digest, + first.run_request_sha256, + expected_pending_action, + final.input_edge_sequence, + final.authority_sequence, + final_acknowledged.runtime_delivery_sequence + if final_acknowledged is not None + else 0, + expected_receipt_absence, + ): + raise ValueError("result-loss closure delivery snapshot is invalid") + if ( + result_loss.delivery_result_loss_closure_artifact_sha256 + != artifact.artifact_sha256() + or result_loss.observed_at != payload.result_loss_observed_at + or result_loss.pending_permit_artifact_sha256 != expected_pending_digest + or result_loss.run_request_sha256 != first.run_request_sha256 + or result_loss.pending_action_request_sha256 != expected_pending_action + ): + raise ValueError("result-loss evidence differs from its authority closure") + child_started = _parse_utc( + result_loss.child_started_at, + field="managed child started_at", + ) + first_issued = _parse_utc(first.issued_at, field="first permit issued_at") + observed = _parse_utc( + result_loss.observed_at, + field="managed result loss observed_at", + ) + closed = _parse_utc(payload.closed_at, field="result-loss closure closed_at") + terminal_verified = _parse_utc( + terminal_verified_at, + field="terminal verified_at", + ) + registry_expires = _parse_utc( + first.qualification_signer_registry_expires_at, + field="result-loss closure registry expires_at", + ) + if not child_started <= first_issued <= observed <= closed <= terminal_verified: + raise ValueError("result-loss closure chronology is invalid") + if not terminal_verified < registry_expires: + raise ValueError("result-loss closure signer registry is expired") + if any( + _parse_utc(item.issued_at, field="permit issued_at") > closed + or _parse_utc(item.delivered_at, field="permit delivered_at") > closed + for item in permit_chain.entries + ): + raise ValueError("result-loss closure excludes an acknowledged permit") + if ( + pending is not None + and _parse_utc( + pending.issued_at, + field="pending permit issued_at", + ) + > observed + ): + raise ValueError("result-loss closure pending permit chronology is invalid") + return artifact.artifact_sha256() + + def build_production_evidence_manifests( prepared: PreparedProductionTerminalEvidence, *, @@ -2091,6 +2477,16 @@ class ProductionTerminalVerificationPayload(ClosedSignedModel): run_report_object_version: str = Field(pattern=_ID_RE) run_report_object_sha256: str = Field(pattern=_SHA256_RE) evidence_manifests: ProductionEvidenceManifests + managed_result_loss: ManagedResultLossEvidence | None = Field( + default=None, + exclude_if=lambda value: value is None, + ) + delivery_result_loss_closure: ProductionDeliveryResultLossClosureArtifact | None = ( + Field( + default=None, + exclude_if=lambda value: value is None, + ) + ) verified_at: str issued_at: str @@ -2243,6 +2639,55 @@ def _closed_terminal_outcome(self) -> "ProductionTerminalVerificationPayload": "terminal run receipt does not match its execution outcome" ) transaction_outcome = receipt.transaction_outcome + result_loss = self.managed_result_loss + result_loss_closure = self.delivery_result_loss_closure + if (result_loss is not None) != ( + receipt.managed_result_loss_evidence_sha256 is not None + ): + raise ValueError("managed result loss receipt binding is incomplete") + if (result_loss is not None) != (result_loss_closure is not None): + raise ValueError("managed result loss authority closure is incomplete") + if result_loss is not None: + pending = chain.pending + if ( + transaction_outcome != "RECONCILIATION_REQUIRED" + or self.permit_count < 1 + or result_loss.evidence_sha256 + != receipt.managed_result_loss_evidence_sha256 + or result_loss.evidence_sha256 + != outcome.managed_result_loss_evidence_sha256 + or result_loss.run_id != self.run_id + or result_loss.flow_run_id_sha256 != self.flow_run_id_sha256 + or result_loss.execution_authority_id != self.execution_authority_id + or result_loss.execution_authority_sha256 + != self.execution_authority_sha256 + or result_loss.execution_authority_signer_sha256 + != self.execution_authority_signer_sha256 + or result_loss.pending_permit_artifact_sha256 + != (pending.permit_artifact_sha256 if pending is not None else None) + or result_loss.run_request_sha256 + != (chain.pending or chain.entries[0]).run_request_sha256 + or result_loss.pending_action_request_sha256 + != (pending.action_request_sha256 if pending is not None else None) + or manifests.identity.required != 0 + or manifests.identity.confirmed != 0 + or manifests.identity.results + or manifests.postcondition.required != 0 + or manifests.postcondition.confirmed != 0 + or manifests.postcondition.records + or manifests.effect.required != 0 + or manifests.effect.confirmed != 0 + or manifests.effect.records + ): + raise ValueError("managed result loss terminal binding is invalid") + assert result_loss_closure is not None + verify_production_delivery_result_loss_closure_binding( + result_loss_closure, + permit_chain=chain, + result_loss=result_loss, + tenant_id=self.tenant_id, + terminal_verified_at=self.verified_at, + ) if transaction_outcome == "VERIFIED": if ( outcome.outcome != "VERIFIED" @@ -2274,7 +2719,9 @@ def _closed_terminal_outcome(self) -> "ProductionTerminalVerificationPayload": or self.permit_count < 1 ): raise ValueError("terminal reconciliation proof is invalid") - if chain.pending is not None: + if result_loss is not None: + pass + elif chain.pending is not None: uncertain_records = tuple( item for item in manifests.effect.records @@ -2464,6 +2911,16 @@ class ProductionTerminalVerificationExpected(ClosedSignedModel): run_report_object_version: str = Field(pattern=_ID_RE) run_report_object_sha256: str = Field(pattern=_SHA256_RE) evidence_manifests: ProductionEvidenceManifests + managed_result_loss: ManagedResultLossEvidence | None = Field( + default=None, + exclude_if=lambda value: value is None, + ) + delivery_result_loss_closure: ProductionDeliveryResultLossClosureArtifact | None = ( + Field( + default=None, + exclude_if=lambda value: value is None, + ) + ) @model_validator(mode="after") def _closed_delivery_state(self) -> "ProductionTerminalVerificationExpected": @@ -2482,6 +2939,12 @@ def _closed_delivery_state(self) -> "ProductionTerminalVerificationExpected": self.pending_permit_count == 1 ): raise ValueError("live pending delivery binding is inconsistent") + if (self.managed_result_loss is not None) != ( + self.delivery_result_loss_closure is not None + ): + raise ValueError("live managed result loss closure is incomplete") + if self.managed_result_loss is not None and self.permit_count < 1: + raise ValueError("live managed result loss lacks a retained permit") for claim_id in self.acknowledged_one_use_claim_ids: if _UUID_RE.fullmatch(claim_id) is None: raise ValueError("live delivery claim id is invalid") @@ -2518,6 +2981,12 @@ class ProductionTerminalVerificationContext(ClosedSignedModel): execution_authority_sha256: str = Field(pattern=_SHA256_RE) execution_authority_signer_sha256: str = Field(pattern=_SHA256_RE) permit_chain: ProductionDeliveryPermitChain + delivery_result_loss_closure: ProductionDeliveryResultLossClosureArtifact | None = ( + Field( + default=None, + exclude_if=lambda value: value is None, + ) + ) run_report_object_version: str = Field(pattern=_ID_RE) verified_at: str issued_at: str @@ -2670,6 +3139,8 @@ def build_production_terminal_verification( run_report_object_version=context.run_report_object_version, run_report_object_sha256=prepared.report_sha256, evidence_manifests=manifests, + managed_result_loss=prepared.report.managed_result_loss, + delivery_result_loss_closure=context.delivery_result_loss_closure, verified_at=context.verified_at, issued_at=context.issued_at, ) diff --git a/openadapt_flow/transaction.py b/openadapt_flow/transaction.py index 9ce2de2b..c55b2ace 100644 --- a/openadapt_flow/transaction.py +++ b/openadapt_flow/transaction.py @@ -323,6 +323,14 @@ def classify_transaction_outcome(report: RunReport) -> TransactionOutcome: stopped (policy rejection, cancellation, governed halt, platform fault). """ + if report.managed_result_loss is not None: + # The outer hosted runner fenced the exact delivery chain but lost the + # managed child's terminal result. A delivery acknowledgment can win + # before that fence, so this is neither effect absence nor a platform + # failure. The signed loss record exists only to force reconciliation + # and prohibit retry. + return TransactionOutcome.RECONCILIATION_REQUIRED + coarse = report.execution_outcome if coarse == "VERIFIED": return TransactionOutcome.VERIFIED diff --git a/public-artifacts.json b/public-artifacts.json index 35bd2386..d254dd80 100644 --- a/public-artifacts.json +++ b/public-artifacts.json @@ -5781,7 +5781,7 @@ }, { "path": "tests/fixtures/terminal_verification_v2_terminal_vectors.json", - "sha256": "82d609a0c29593c91ec42b9416a64d6432fad47e3898bbfb926e76f7598f91df" + "sha256": "314c902ca5420d312626d521cec05652848f503e76226cb7168ed2384c759376" }, { "path": "tests/fixtures/theme-region-stable-overhalt-v1.16.1.json", diff --git a/scripts/generate_terminal_verification_v2_vectors.py b/scripts/generate_terminal_verification_v2_vectors.py index 2b7ea7f3..e02cb3c2 100644 --- a/scripts/generate_terminal_verification_v2_vectors.py +++ b/scripts/generate_terminal_verification_v2_vectors.py @@ -13,29 +13,43 @@ from openadapt_flow.qualification_admission_v2 import canonical_json from openadapt_flow.terminal_verification_v2 import ( + RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN, + RESULT_LOSS_CLOSURE_REQUEST_DOMAIN, SIGNATURE_DOMAIN, evidence_runner_key_id, evidence_runner_signer_sha256, sign_production_terminal_verification, ) from tests.test_terminal_verification_v2 import ( + _acknowledged_reconciliation_payload, _halted_payload, + _managed_result_loss_acknowledged_payload, + _managed_result_loss_payload, + _payload, _private_key, _reconciliation_payload, + _result_loss_request, ) DEFAULT_OUTPUT = Path("tests/fixtures/terminal_verification_v2_terminal_vectors.json") def _vector(name: str, *, uncertain_delivery: bool) -> dict[str, object]: - payload = ( - _halted_payload() - if name == "halted-before-effect-zero-permit" - else _reconciliation_payload() - ) + payload = { + "verified-complete": _payload, + "halted-before-effect-zero-permit": _halted_payload, + "reconciliation-required-pending-permit": _reconciliation_payload, + "reconciliation-required-acknowledged-inconclusive": ( + _acknowledged_reconciliation_payload + ), + "reconciliation-required-managed-result-loss": _managed_result_loss_payload, + "reconciliation-required-managed-result-loss-acknowledged": ( + _managed_result_loss_acknowledged_payload + ), + }[name]() envelope = sign_production_terminal_verification(payload, _private_key()) raw = canonical_json(envelope) - effect_state = payload.evidence_manifests.effect.records[0] + effect_records = payload.evidence_manifests.effect.records return { "callback": { "artifact_bytes_source": "envelope_canonical_base64", @@ -46,7 +60,9 @@ def _vector(name: str, *, uncertain_delivery: bool) -> dict[str, object]: "started": True, "uncertain_delivery": uncertain_delivery, }, - "effect_state": effect_state.model_dump(mode="json"), + "effect_state": ( + effect_records[0].model_dump(mode="json") if effect_records else None + ), "envelope_canonical_base64": b64encode(raw).decode("ascii"), "name": name, "payload_canonical_sha256": hashlib.sha256( @@ -54,6 +70,11 @@ def _vector(name: str, *, uncertain_delivery: bool) -> dict[str, object]: ).hexdigest(), "signature": envelope.signature, "terminal_verification_artifact_sha256": envelope.artifact_sha256(), + "managed_result_loss": ( + payload.managed_result_loss.model_dump(mode="json") + if payload.managed_result_loss is not None + else None + ), } @@ -68,6 +89,22 @@ def build_fixture() -> dict[str, object]: encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw, ) + managed = _managed_result_loss_payload() + request = _result_loss_request() + closure = managed.delivery_result_loss_closure + assert closure is not None + closure_raw = canonical_json(closure) + chain_raw = canonical_json(managed.permit_chain) + closure_result = { + "schema_version": ( + "openadapt.production-delivery-result-loss-closure-result/v2" + ), + "status": "closed", + "closure_artifact_bytes_base64": b64encode(closure_raw).decode("ascii"), + "closure_artifact_sha256": closure.artifact_sha256(), + "permit_chain_bytes_base64": b64encode(chain_raw).decode("ascii"), + "permit_chain_sha256": managed.permit_chain.permit_chain_sha256, + } return { "key_id": evidence_runner_key_id(public_bytes), "private_key_base64": b64encode(private_bytes).decode("ascii"), @@ -75,7 +112,34 @@ def build_fixture() -> dict[str, object]: "schema_version": "openadapt.production-terminal-cross-language-vectors/v2", "signature_domain_base64": b64encode(SIGNATURE_DOMAIN).decode("ascii"), "signer_sha256": evidence_runner_signer_sha256(public_bytes), + "managed_result_loss_closure_vector": { + "http_method": "POST", + "http_route": "/api/internal/managed-delivery-result-loss-closure", + "authorization_credential_source": "hosted_dispatch.lease_token", + "request_digest_domain_base64": b64encode( + RESULT_LOSS_CLOSURE_REQUEST_DOMAIN + ).decode("ascii"), + "request_canonical_base64": b64encode(request.canonical_bytes()).decode( + "ascii" + ), + "request_sha256": request.request_sha256(), + "payload_signature_domain_base64": b64encode( + RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN + ).decode("ascii"), + "closure_payload_sha256": closure.payload_sha256, + "closure_artifact_canonical_base64": b64encode(closure_raw).decode("ascii"), + "closure_artifact_sha256": closure.artifact_sha256(), + "permit_chain_canonical_base64": b64encode(chain_raw).decode("ascii"), + "permit_chain_sha256": managed.permit_chain.permit_chain_sha256, + "result_canonical_base64": b64encode(canonical_json(closure_result)).decode( + "ascii" + ), + }, "vectors": [ + _vector( + "verified-complete", + uncertain_delivery=False, + ), _vector( "halted-before-effect-zero-permit", uncertain_delivery=False, @@ -84,6 +148,18 @@ def build_fixture() -> dict[str, object]: "reconciliation-required-pending-permit", uncertain_delivery=True, ), + _vector( + "reconciliation-required-acknowledged-inconclusive", + uncertain_delivery=False, + ), + _vector( + "reconciliation-required-managed-result-loss", + uncertain_delivery=True, + ), + _vector( + "reconciliation-required-managed-result-loss-acknowledged", + uncertain_delivery=False, + ), ], } diff --git a/tests/fixtures/terminal_verification_v2_terminal_vectors.json b/tests/fixtures/terminal_verification_v2_terminal_vectors.json index 5d7ceabc..e175c30f 100644 --- a/tests/fixtures/terminal_verification_v2_terminal_vectors.json +++ b/tests/fixtures/terminal_verification_v2_terminal_vectors.json @@ -1,11 +1,53 @@ { "key_id": "evidence-runner-ed25519-56475aa75463474c", + "managed_result_loss_closure_vector": { + "authorization_credential_source": "hosted_dispatch.lease_token", + "closure_artifact_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImF1dGhlbnRpY2F0ZWRfcnVubmVyX2lkX3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJhdXRoZW50aWNhdGVkX3Nlc3Npb25faWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImJsaW5kX3JldHJ5X2F1dGhvcml6ZWQiOmZhbHNlLCJjaGlsZF9zdGFydF9ldmlkZW5jZV9zaGEyNTYiOiI2NWE2YmVkOTIzZTQ1YTZhYTBjNjI0MzA0NWUwMzY0YzkwYWNlMDY0OGZlY2I1YjBiYjJlYmQ5NDAyNzkzYzc3IiwiY2hpbGRfc3RhcnRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiY2xvc2VkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJjbG9zdXJlX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDExIiwiY2xvc3VyZV9yZXF1ZXN0X3NoYTI1NiI6ImJhZjQxN2FlNDhlZWM0MDY1ODRlZjUyODZhYWExYjFhYzJlNmUwM2Q4YTI5YTRmYjRiMGM1MzI3MDRiZTU5YzYiLCJjbG9zdXJlX3NlcXVlbmNlIjoxLCJkZWxpdmVyeV9hY2tub3dsZWRnbWVudF9hdXRob3JpemVkIjpmYWxzZSwiZGVsaXZlcnlfc3RhdGUiOiJDTE9TRURfVU5SRVNPTFZFRF9SRVNVTFRfTE9TUyIsImRpc3BhdGNoX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA5IiwiZGlzcGF0Y2hfc2Vzc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsImVmZmVjdF9hYnNlbmNlX2NsYWltZWQiOmZhbHNlLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImZpbmFsX2F1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZmluYWxfaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiZmluYWxfcnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6MCwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW1wb3RlbmN5X2tleV9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuYWdlZF9kaXNwYXRjaF9iaW5kaW5nX3NoYTI1NiI6InNoYTI1NjpjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwibmV3X3Blcm1pdF9hdXRob3JpemVkIjpmYWxzZSwibm90X3JlY2VpdmVkX2NsYWltZWQiOmZhbHNlLCJwZW5kaW5nX2FjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJwZW5kaW5nX3Blcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5IiwicGVuZGluZ19wZXJtaXRfY291bnQiOjEsInBlcm1pdF9jaGFpbl9zaGEyNTYiOiI1OWIzOTk3MWUzMDUxMDljNTQyOTk0NWE1ZmFiNDY2MGIwOWY0MWU3YWMzZmZkOTJlMzUwNmFmMDAzMzdkYTk1IiwicGVybWl0X2NvdW50IjoxLCJyZWNlaXB0X2Fic2VuY2Vfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInJlc3VsdF9sb3NzX29ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwicnVuX3N0b3JlX2lkZW50aXR5X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtcGF5bG9hZC92MiIsInRlbmFudF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMiIsInRlcm1pbmFsX2NhbGxiYWNrX3JlcXVpcmVkIjp0cnVlfSwicGF5bG9hZF9zaGEyNTYiOiI4ZGI1OGY2YTA1ZjRlZjFmODI3NzY2MDBkNWUyYTEzMDY0NWEzZTEyYjUwZmVlNjgxMDg4NzRlZmYwOGY3ZWJmIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZXN1bHQtbG9zcy1jbG9zdXJlLWFydGlmYWN0L3YyIiwic2lnbmF0dXJlIjoiYWpDYXd4d1kxWWlBVTF3cDNrdXBZR2t6bmw3MlZBVHBsSFdnWTF1WDU3akVPRmpfNHMyX3ZYUGxVRUF5VW1wTmhwNHhvcVRYQlA4YzhPZTJKTGUzRHciLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImRlbGl2ZXJ5LWF1dGhvcml0eS1lZDI1NTE5LTI0ZjZlZDZhY2JmZTEwMDkiLCJwdWJsaWNfa2V5IjoiS2F5NjRVRzh5dkN5TGhxVTAwMEx4elllVW0wTC9oTElsNVM4a3lLV2JkYz0ifX0=", + "closure_artifact_sha256": "cec357acdb74f4e02dd1c3209a8ec880915c9830136e07260f3c1f631f8d52e8", + "closure_payload_sha256": "8db58f6a05f4ef1f82776600d5e2a130645a3e12b50fee68108874eff08f7ebf", + "http_method": "POST", + "http_route": "/api/internal/managed-delivery-result-loss-closure", + "payload_signature_domain_base64": "b3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1wYXlsb2FkLnYyAA==", + "permit_chain_canonical_base64": "eyJlbnRyaWVzIjpbXSwicGVuZGluZyI6eyJhY3R1YXRpb25fcmVwbGF5X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0IjpudWxsLCJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0X3NoYTI1NiI6bnVsbCwiZGVsaXZlcnlfc3RhdGUiOiJVTlJFU09MVkVEIiwicGVybWl0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJwZXJtaXRfaWQiOiJwZXJtaXQ6MSIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2NoZWNrZWRfYXQiOiIyMDI2LTA4LTE4VDExOjU5OjMwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2V4cGlyZXNfYXQiOiIyMDI2LTA4LTIwVDExOjAwOjAwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiJjODdmNTc1MWFhOWIyNzljY2UyM2FhMTc4YWZmOTU2ZTE5ZjY5MGZjZDJmOWY3MGUwZDk5OTc4MmE2OTBkNmI4Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiIwQnlIZ3FEeFFacm5nTElGLTZ0Wk9xOWxYYW1jQVFDMGRvVVRuZUtOUGVTLVN5b09iUXlPa0F1UEZLNVBZY056T2JaeGdzZ0FMZFdhSV9zYVVDcGxCZyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwicGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJyZWNlaXB0X2Fic2VuY2Vfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVuZGluZy1wZXJtaXQvdjIifSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi92MiJ9", + "permit_chain_sha256": "59b39971e305109c5429945a5fab4660b09f41e7ac3ffd92e3506af00337da95", + "request_canonical_base64": "eyJjaGlsZF9zdGFydF9ldmlkZW5jZSI6eyJhdXRoZW50aWNhdGVkX3J1bm5lcl9pZF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYXV0aGVudGljYXRlZF9zZXNzaW9uX2lkX3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJkaXNwYXRjaF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOSIsImRpc3BhdGNoX3Nlc3Npb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTAiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsIm1hbmFnZWRfZGlzcGF0Y2hfYmluZGluZ19zaGEyNTYiOiJzaGEyNTY6Y2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsIm1hcmtlcl9zaGEyNTYiOiI2NWE2YmVkOTIzZTQ1YTZhYTBjNjI0MzA0NWUwMzY0YzkwYWNlMDY0OGZlY2I1YjBiYjJlYmQ5NDAyNzkzYzc3IiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3N0b3JlX2lkZW50aXR5X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5tYW5hZ2VkLWNoaWxkLXN0YXJ0L3YxIiwic3RhcnRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIn0sImV4cGVjdGVkX2Nsb3N1cmVfc2VxdWVuY2UiOjAsInJlc3VsdF9sb3NzX29ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtcmVxdWVzdC92MiJ9", + "request_digest_domain_base64": "b3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1yZXF1ZXN0LnYyAA==", + "request_sha256": "baf417ae48eec406584ef5286aaa1b1ac2e6e03d8a29a4fb4b0c532704be59c6", + "result_canonical_base64": "eyJjbG9zdXJlX2FydGlmYWN0X2J5dGVzX2Jhc2U2NCI6ImV5SndZWGxzYjJGa0lqcDdJbUZqYTI1dmQyeGxaR2RsWkY5d1pYSnRhWFJmWTI5MWJuUWlPakFzSW1GamRIVmhkR2x2Ymw5eVpYQnNZWGxmWVhWMGFHOXlhWHBsWkNJNlptRnNjMlVzSW1GMWRHaGxiblJwWTJGMFpXUmZjblZ1Ym1WeVgybGtYM05vWVRJMU5pSTZJbUZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0VpTENKaGRYUm9aVzUwYVdOaGRHVmtYM05sYzNOcGIyNWZhV1JmYzJoaE1qVTJJam9pWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWWlJc0ltSnNhVzVrWDNKbGRISjVYMkYxZEdodmNtbDZaV1FpT21aaGJITmxMQ0pqYUdsc1pGOXpkR0Z5ZEY5bGRtbGtaVzVqWlY5emFHRXlOVFlpT2lJMk5XRTJZbVZrT1RJelpUUTFZVFpoWVRCak5qSTBNekEwTldVd016WTBZemt3WVdObE1EWTBPR1psWTJJMVlqQmlZakpsWW1RNU5EQXlOemt6WXpjM0lpd2lZMmhwYkdSZmMzUmhjblJsWkY5aGRDSTZJakl3TWpZdE1EZ3RNVGhVTVRJNk1EQTZNREJhSWl3aVkyeHZjMlZrWDJGMElqb2lNakF5Tmkwd09DMHhPRlF4TWpvd01Eb3dNbG9pTENKamJHOXpkWEpsWDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01ERXhJaXdpWTJ4dmMzVnlaVjl5WlhGMVpYTjBYM05vWVRJMU5pSTZJbUpoWmpReE4yRmxORGhsWldNME1EWTFPRFJsWmpVeU9EWmhZV0V4WWpGaFl6SmxObVV3TTJRNFlUSTVZVFJtWWpSaU1HTTFNekkzTURSaVpUVTVZellpTENKamJHOXpkWEpsWDNObGNYVmxibU5sSWpveExDSmtaV3hwZG1WeWVWOWhZMnR1YjNkc1pXUm5iV1Z1ZEY5aGRYUm9iM0pwZW1Wa0lqcG1ZV3h6WlN3aVpHVnNhWFpsY25sZmMzUmhkR1VpT2lKRFRFOVRSVVJmVlU1U1JWTlBURlpGUkY5U1JWTlZURlJmVEU5VFV5SXNJbVJwYzNCaGRHTm9YMmxrSWpvaU1EQXdNREF3TURBdE1EQXdNQzAwTURBd0xUZ3dNREF0TURBd01EQXdNREF3TURBNUlpd2laR2x6Y0dGMFkyaGZjMlZ6YzJsdmJsOXBaQ0k2SWpBd01EQXdNREF3TFRBd01EQXROREF3TUMwNE1EQXdMVEF3TURBd01EQXdNREF4TUNJc0ltVm1abVZqZEY5aFluTmxibU5sWDJOc1lXbHRaV1FpT21aaGJITmxMQ0psZUdWamRYUnBiMjVmWVhWMGFHOXlhWFI1WDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01EQTRJaXdpWlhobFkzVjBhVzl1WDJGMWRHaHZjbWwwZVY5emFHRXlOVFlpT2lKaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaElpd2laWGhsWTNWMGFXOXVYMkYxZEdodmNtbDBlVjl6YVdkdVpYSmZjMmhoTWpVMklqb2lNalJtTm1Wa05tRmpZbVpsTVRBd09XTXdNekJrTjJOaE5UWTNZek16WTJFME9ETXdPVEV4TkRrNE1qTTJZalUxTmpGaE5tTTRNbUZpWldNMVpHVXlPQ0lzSW1acGJtRnNYMkYxZEdodmNtbDBlVjl6WlhGMVpXNWpaU0k2TUN3aVptbHVZV3hmYVc1d2RYUmZaV1JuWlY5elpYRjFaVzVqWlNJNk1Td2labWx1WVd4ZmNuVnVkR2x0WlY5a1pXeHBkbVZ5ZVY5elpYRjFaVzVqWlNJNk1Dd2labXh2ZDE5eWRXNWZhV1JmYzJoaE1qVTJJam9pTVRGbE5UazBaalE0TVRrMU9HTXhNR1V6TURFMVpEQmlaakEwTkRkaE1qSm1NRFk0WVRoaE5qUTNaalEzTldSbU1UVmpaVEpqTjJGaU5HSTRaak5tTVNJc0ltbGtaVzF3YjNSbGJtTjVYMnRsZVY5emFHRXlOVFlpT2lKa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa0lpd2liV0Z1WVdkbFpGOWthWE53WVhSamFGOWlhVzVrYVc1blgzTm9ZVEkxTmlJNkluTm9ZVEkxTmpwalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOaklpd2libVYzWDNCbGNtMXBkRjloZFhSb2IzSnBlbVZrSWpwbVlXeHpaU3dpYm05MFgzSmxZMlZwZG1Wa1gyTnNZV2x0WldRaU9tWmhiSE5sTENKd1pXNWthVzVuWDJGamRHbHZibDl5WlhGMVpYTjBYM05vWVRJMU5pSTZJbVJrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1FpTENKd1pXNWthVzVuWDNCbGNtMXBkRjloY25ScFptRmpkRjl6YUdFeU5UWWlPaUk1TnpKaE9ESmhaalExTm1SaE5UUXlZbUZqTnpnME5qQXhPRGc1TmpZNE9Ea3haRGhpTjJOalpESTBNamt3TkRneE5tSXpNamt5WlRFMU4yVTFNamc1SWl3aWNHVnVaR2x1WjE5d1pYSnRhWFJmWTI5MWJuUWlPakVzSW5CbGNtMXBkRjlqYUdGcGJsOXphR0V5TlRZaU9pSTFPV0l6T1RrM01XVXpNRFV4TURsak5UUXlPVGswTldFMVptRmlORFkyTUdJd09XWTBNV1UzWVdNelptWmtPVEpsTXpVd05tRm1NREF6TXpka1lUazFJaXdpY0dWeWJXbDBYMk52ZFc1MElqb3hMQ0p5WldObGFYQjBYMkZpYzJWdVkyVmZiMkp6WlhKMlpXUmZZWFFpT2lJeU1ESTJMVEE0TFRFNFZERXlPakF3T2pBeVdpSXNJbkpsYzNWc2RGOXNiM056WDI5aWMyVnlkbVZrWDJGMElqb2lNakF5Tmkwd09DMHhPRlF4TWpvd01Eb3dNbG9pTENKeWRXNWZhV1FpT2lJd01EQXdNREF3TUMwd01EQXdMVFF3TURBdE9EQXdNQzB3TURBd01EQXdNREF3TURFaUxDSnlkVzVmY21WeGRXVnpkRjl6YUdFeU5UWWlPaUpqWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qSWl3aWNuVnVYM04wYjNKbFgybGtaVzUwYVhSNVgzTm9ZVEkxTmlJNkltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSWlMQ0p6WTJobGJXRmZkbVZ5YzJsdmJpSTZJbTl3Wlc1aFpHRndkQzV3Y205a2RXTjBhVzl1TFdSbGJHbDJaWEo1TFhKbGMzVnNkQzFzYjNOekxXTnNiM04xY21VdGNHRjViRzloWkM5Mk1pSXNJblJsYm1GdWRGOXBaQ0k2SWpBd01EQXdNREF3TFRBd01EQXROREF3TUMwNE1EQXdMVEF3TURBd01EQXdNREF3TWlJc0luUmxjbTFwYm1Gc1gyTmhiR3hpWVdOclgzSmxjWFZwY21Wa0lqcDBjblZsZlN3aWNHRjViRzloWkY5emFHRXlOVFlpT2lJNFpHSTFPR1kyWVRBMVpqUmxaakZtT0RJM056WTJNREJrTldVeVlURXpNRFkwTldFelpURXlZalV3Wm1WbE5qZ3hNRGc0TnpSbFptWXdPR1kzWldKbUlpd2ljMk5vWlcxaFgzWmxjbk5wYjI0aU9pSnZjR1Z1WVdSaGNIUXVjSEp2WkhWamRHbHZiaTFrWld4cGRtVnllUzF5WlhOMWJIUXRiRzl6Y3kxamJHOXpkWEpsTFdGeWRHbG1ZV04wTDNZeUlpd2ljMmxuYm1GMGRYSmxJam9pWVdwRFlYZDRkMWt4V1dsQlZURjNjRE5yZFhCWlIydDZibXczTWxaQlZIQnNTRmRuV1RGMVdEVTNha1ZQUm1wZk5ITXlYM1pZVUd4VlJVRjVWVzF3VG1od05IaHZjVlJZUWxBNFl6aFBaVEpLVEdVelJIY2lMQ0p6YVdkdVpYSWlPbnNpWVd4bmIzSnBkR2h0SWpvaVpXUXlOVFV4T1NJc0ltdGxlVjlwWkNJNkltUmxiR2wyWlhKNUxXRjFkR2h2Y21sMGVTMWxaREkxTlRFNUxUSTBaalpsWkRaaFkySm1aVEV3TURraUxDSndkV0pzYVdOZmEyVjVJam9pUzJGNU5qUlZSemg1ZGtONVRHaHhWVEF3TUV4NGVsbGxWVzB3VEM5b1RFbHNOVk00YTNsTFYySmtZejBpZlgwPSIsImNsb3N1cmVfYXJ0aWZhY3Rfc2hhMjU2IjoiY2VjMzU3YWNkYjc0ZjRlMDJkZDFjMzIwOWE4ZWM4ODA5MTVjOTgzMDEzNmUwNzI2MGYzYzFmNjMxZjhkNTJlOCIsInBlcm1pdF9jaGFpbl9ieXRlc19iYXNlNjQiOiJleUpsYm5SeWFXVnpJanBiWFN3aWNHVnVaR2x1WnlJNmV5SmhZM1IxWVhScGIyNWZjbVZ3YkdGNVgyRjFkR2h2Y21sNlpXUWlPbVpoYkhObExDSmtaV3hwZG1WeWVWOXlaV05sYVhCMFgyRnlkR2xtWVdOMElqcHVkV3hzTENKa1pXeHBkbVZ5ZVY5eVpXTmxhWEIwWDJGeWRHbG1ZV04wWDNOb1lUSTFOaUk2Ym5Wc2JDd2laR1ZzYVhabGNubGZjM1JoZEdVaU9pSlZUbEpGVTA5TVZrVkVJaXdpY0dWeWJXbDBYMkZ5ZEdsbVlXTjBJanA3SW5CaGVXeHZZV1FpT25zaVlXTjBhVzl1WDNKbGNYVmxjM1JmYzJoaE1qVTJJam9pWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkNJc0ltRmtiV2x6YzJsdmJsOWhjblJwWm1GamRGOXphR0V5TlRZaU9pSmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtJaXdpWVhWMGFHOXlhWFI1WDNObGNYVmxibU5sSWpvd0xDSmxiblpwY205dWJXVnVkRjlrYVdkbGMzUWlPaUpoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoSWl3aVpYWnBaR1Z1WTJWZmFXUmxiblJwZEhsZmMyaGhNalUySWpvaVpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpTSXNJbVY0WldOMWRHbHZibDloZFhSb2IzSnBkSGxmYVdRaU9pSXdNREF3TURBd01DMHdNREF3TFRRd01EQXRPREF3TUMwd01EQXdNREF3TURBd01EZ2lMQ0psZUdWamRYUnBiMjVmWVhWMGFHOXlhWFI1WDNOb1lUSTFOaUk2SW1GaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdFaUxDSm1iRzkzWDNKMWJsOXBaRjl6YUdFeU5UWWlPaUl4TVdVMU9UUm1ORGd4T1RVNFl6RXdaVE13TVRWa01HSm1NRFEwTjJFeU1tWXdOamhoT0dFMk5EZG1ORGMxWkdZeE5XTmxNbU0zWVdJMFlqaG1NMll4SWl3aWFXNXdkWFJmWldSblpWOXpaWEYxWlc1alpTSTZNU3dpYVhOemRXVmtYMkYwSWpvaU1qQXlOaTB3T0MweE9GUXhNam93TURvd01Gb2lMQ0p3WlhKdGFYUmZhV1FpT2lKd1pYSnRhWFE2TVNJc0luRjFZV3hwWm1sallYUnBiMjVmYzJsbmJtVnlYM0psWjJsemRISjVYMk5vWldOclpXUmZZWFFpT2lJeU1ESTJMVEE0TFRFNFZERXhPalU1T2pNd1dpSXNJbkYxWVd4cFptbGpZWFJwYjI1ZmMybG5ibVZ5WDNKbFoybHpkSEo1WDJWNGNHbHlaWE5mWVhRaU9pSXlNREkyTFRBNExUSXdWREV4T2pBd09qQXdXaUlzSW5GMVlXeHBabWxqWVhScGIyNWZjMmxuYm1WeVgzSmxaMmx6ZEhKNVgzSmxkbWx6YVc5dUlqbzNMQ0p4ZFdGc2FXWnBZMkYwYVc5dVgzTnBaMjVsY2w5eVpXZHBjM1J5ZVY5emFHRXlOVFlpT2lKbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbElpd2ljblZ1WDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01EQXhJaXdpY25WdVgzSmxjWFZsYzNSZmMyaGhNalUySWpvaVkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOall5SXNJbk5qYUdWdFlWOTJaWEp6YVc5dUlqb2liM0JsYm1Ga1lYQjBMbkJ5YjJSMVkzUnBiMjR0WkdWc2FYWmxjbmt0Y0dWeWJXbDBMWEJoZVd4dllXUXZkaklpZlN3aWNHRjViRzloWkY5emFHRXlOVFlpT2lKak9EZG1OVGMxTVdGaE9XSXlOemxqWTJVeU0yRmhNVGM0WVdabU9UVTJaVEU1WmpZNU1HWmpaREptT1dZM01HVXdaRGs1T1RjNE1tRTJPVEJrTm1JNElpd2ljMk5vWlcxaFgzWmxjbk5wYjI0aU9pSnZjR1Z1WVdSaGNIUXVjSEp2WkhWamRHbHZiaTFrWld4cGRtVnllUzF3WlhKdGFYUXRZWEowYVdaaFkzUXZkaklpTENKemFXZHVZWFIxY21VaU9pSXdRbmxJWjNGRWVGRmFjbTVuVEVsR0xUWjBXazl4T1d4WVlXMWpRVkZETUdSdlZWUnVaVXRPVUdWVExWTjViMDlpVVhsUGEwRjFVRVpMTlZCWlkwNTZUMkphZUdkelowRk1aRmRoU1Y5ellWVkRjR3hDWnlJc0luTnBaMjVsY2lJNmV5SmhiR2R2Y21sMGFHMGlPaUpsWkRJMU5URTVJaXdpYTJWNVgybGtJam9pWkdWc2FYWmxjbmt0WVhWMGFHOXlhWFI1TFdWa01qVTFNVGt0TWpSbU5tVmtObUZqWW1abE1UQXdPU0lzSW5CMVlteHBZMTlyWlhraU9pSkxZWGsyTkZWSE9IbDJRM2xNYUhGVk1EQXdUSGg2V1dWVmJUQk1MMmhNU1d3MVV6aHJlVXRYWW1SalBTSjlmU3dpY0dWeWJXbDBYMkZ5ZEdsbVlXTjBYM05vWVRJMU5pSTZJamszTW1FNE1tRm1ORFUyWkdFMU5ESmlZV00zT0RRMk1ERTRPRGsyTmpnNE9URmtPR0kzWTJOa01qUXlPVEEwT0RFMllqTXlPVEpsTVRVM1pUVXlPRGtpTENKeVpXTmxhWEIwWDJGaWMyVnVZMlZmYjJKelpYSjJaV1JmWVhRaU9pSXlNREkyTFRBNExURTRWREV5T2pBd09qQXlXaUlzSW5OamFHVnRZVjkyWlhKemFXOXVJam9pYjNCbGJtRmtZWEIwTG5CeWIyUjFZM1JwYjI0dFpHVnNhWFpsY25rdGNHVnVaR2x1Wnkxd1pYSnRhWFF2ZGpJaWZTd2ljR1Z5YldsMFgyTm9ZV2x1WDNOb1lUSTFOaUk2SWpVNVlqTTVPVGN4WlRNd05URXdPV00xTkRJNU9UUTFZVFZtWVdJME5qWXdZakE1WmpReFpUZGhZek5tWm1RNU1tVXpOVEEyWVdZd01ETXpOMlJoT1RVaUxDSnpZMmhsYldGZmRtVnljMmx2YmlJNkltOXdaVzVoWkdGd2RDNXdjbTlrZFdOMGFXOXVMV1JsYkdsMlpYSjVMWEJsY20xcGRDMWphR0ZwYmk5Mk1pSjkiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTliMzk5NzFlMzA1MTA5YzU0Mjk5NDVhNWZhYjQ2NjBiMDlmNDFlN2FjM2ZmZDkyZTM1MDZhZjAwMzM3ZGE5NSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1yZXN1bHQvdjIiLCJzdGF0dXMiOiJjbG9zZWQifQ==" + }, "private_key_base64": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "public_key_base64": "A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=", "schema_version": "openadapt.production-terminal-cross-language-vectors/v2", "signature_domain_base64": "b3BlbmFkYXB0LXByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uLXYyAA==", "signer_sha256": "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c", "vectors": [ + { + "callback": { + "artifact_bytes_source": "envelope_canonical_base64", + "outcome": "VERIFIED", + "report_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "run_id": "00000000-0000-4000-8000-000000000001", + "schema_version": "openadapt.hosted-runner-terminal/v1", + "started": true, + "uncertain_delivery": false + }, + "effect_state": { + "effect_contract_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "final_verdict": "confirmed", + "observed_effect": "present", + "reconciliation_actions": 0, + "reconciliation_completed": false, + "result_index": 0, + "verification_tier": 1, + "verifier_identity": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjEsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6ImYwY2ExOWE2YjNlYWJlNTk0ZGJjOTA2ODljMmY3NGNiZTVlODJkNGQ5NDdkYjI5YTVkMDdiMTY0MWU4OTYxY2YiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjoxLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6ImEzOThlNGJmMDUyZmIwMzBiOTdlNDM1ZTI4M2E2ZjY2MzJkOWE1ZDUxMjU0YTYwZDY4OWUwNWEwZGY2ZjAxMDYiLCJyZWNvcmRzIjpbeyJlZmZlY3RfY29udHJhY3RfaGFzaCI6InNoYTI1NjphYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZmluYWxfdmVyZGljdCI6ImNvbmZpcm1lZCIsIm9ic2VydmVkX2VmZmVjdCI6InByZXNlbnQiLCJyZWNvbmNpbGlhdGlvbl9hY3Rpb25zIjowLCJyZWNvbmNpbGlhdGlvbl9jb21wbGV0ZWQiOmZhbHNlLCJyZXN1bHRfaW5kZXgiOjAsInZlcmlmaWNhdGlvbl90aWVyIjoxLCJ2ZXJpZmllcl9pZGVudGl0eSI6InNoYTI1NjpiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIn1dLCJyZXF1aXJlZCI6MSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1lZmZlY3QtZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sImlkZW50aXR5Ijp7ImNvbmZpcm1lZCI6MSwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsIm1hbmlmZXN0X3NoYTI1NiI6IjEzNWM4NjNlZGM3ZmFiMThjYTI5YjQ5MjkzZmMyNWExYzliZDRkZTBlOWVkMjhkNGNlYzZlOGQwYTY3OTEyMjkiLCJyZXF1aXJlZCI6MSwicmVzdWx0cyI6W3sibW9kZSI6InN0cnVjdHVyZWQiLCJyZXN1bHRfaW5kZXgiOjAsInNpZ25hbHMiOltdLCJzdGF0dXMiOiJ2ZXJpZmllZCJ9XSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1pZGVudGl0eS1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwicG9saWN5Ijp7ImFkbWlzc2lvbl9wb2xpY3lfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImVmZmVjdF9jb250cmFjdF9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZW52aXJvbm1lbnRfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJnb3Zlcm5lZF9wb2xpY3lfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImdvdmVybmVkX3J1bnRpbWVfaW5wdXRzX2RpZ2VzdCI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiNGY5ODhkMDE1YmNkN2NkN2UwNWJhMDY2M2Q4OWYwYTk2OGYwZDhhY2U5OWIwOTBlYmFiZWU1MmExYzU1MDQyMSIsIm1pbmltdW1fZWZmZWN0X3RpZXIiOjEsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9saWN5LWV2aWRlbmNlL3YxIn0sInBvc3Rjb25kaXRpb24iOnsiY29uZmlybWVkIjoxLCJtYW5pZmVzdF9zaGEyNTYiOiJmNjg1YWUyOWYzMjQyMzdiMzcxOWNiMmVkMTZkNmYzMjVmYjg4Yjk4Y2E0NmQ1MjNjY2I1MDJiYWFjOGQ5YjU5IiwicmVjb3JkcyI6W3siYWN0aW9uX2tpbmQiOiJjbGljayIsImFjdHVhdGlvbl9wYXRoIjoiZ3VpIiwiY29udHJhY3RfaW5kZXgiOjAsImNvbnRyYWN0X2tpbmQiOiJleHBsaWNpdF9wcmVkaWNhdGUiLCJjb250cmFjdF9zaGEyNTYiOiIwYWRlMGI1MjBiYzAwNzg5MWNhY2NhMDAxNzE1Nzg0NDJhMmE3NGNlZjE1ZmY2NGRkZTQ1ZGYwN2FmYjY0YWViIiwicmVzdWx0X2luZGV4IjowLCJzdGVwX2NvbnRyYWN0X3NoYTI1NiI6IjFhNTk1MzZhNDg4YjFjNDZhYWNmMGJhMzc5MzBiODQwNjJjMTQyNzU1ZmE5MzkxNTJlZjVlMjhjNDA5NDcxZWQiLCJzdGVwX2luZGV4IjowLCJ2ZXJkaWN0IjoicGFzc2VkIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9XSwicmVxdWlyZWQiOjEsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9zdGNvbmRpdGlvbi1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifX0sImV2aWRlbmNlX3J1bm5lcl9zaWduZXJfc2hhMjU2IjoiNTY0NzVhYTc1NDYzNDc0YzAyODVkZjVkYmYyYmNhYjczZGE2NTEzNTg4MzllOWI3NzQ4MWIyZWFiMTA3NzA4YyIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZXhlY3V0aW9uX291dGNvbWUiOnsiY29tcGVuc2F0aW9uX2FjdGlvbnMiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiIsImVmZmVjdF90aWVyXzEiLCJpZGVudGl0eSIsInBvc3Rjb25kaXRpb24iXSwiZXhlY3V0aW9uX2NvbXBsZXRlZCI6dHJ1ZSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IlZFUklGSUVEIiwicGFzc2VkX2NvbnRyYWN0cyI6eyJhdXRob3JpemF0aW9uIjoxLCJlZmZlY3QiOjEsImlkZW50aXR5IjoxLCJwb3N0Y29uZGl0aW9uIjoxfSwicG9zdGNvbmRpdGlvbl9ldmlkZW5jZSI6W3siYWN0aW9uX2tpbmQiOiJjbGljayIsImFjdHVhdGlvbl9wYXRoIjoiZ3VpIiwiY29udHJhY3RfaW5kZXgiOjAsImNvbnRyYWN0X2tpbmQiOiJleHBsaWNpdF9wcmVkaWNhdGUiLCJjb250cmFjdF9zaGEyNTYiOiIwYWRlMGI1MjBiYzAwNzg5MWNhY2NhMDAxNzE1Nzg0NDJhMmE3NGNlZjE1ZmY2NGRkZTQ1ZGYwN2FmYjY0YWViIiwicmVzdWx0X2luZGV4IjowLCJzdGVwX2NvbnRyYWN0X3NoYTI1NiI6IjFhNTk1MzZhNDg4YjFjNDZhYWNmMGJhMzc5MzBiODQwNjJjMTQyNzU1ZmE5MzkxNTJlZjVlMjhjNDA5NDcxZWQiLCJzdGVwX2luZGV4IjowLCJ2ZXJkaWN0IjoicGFzc2VkIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9XSwicHJvZHVjdGlvbl9lbGlnaWJsZSI6dHJ1ZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicXVhbGlmaWNhdGlvbl9ldmlkZW5jZV9vbmx5IjpmYWxzZSwicmVxdWlyZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MSwiaWRlbnRpdHkiOjEsInBvc3Rjb25kaXRpb24iOjF9LCJ2ZXJzaW9uIjoib3BlbmFkYXB0LmV4ZWN1dGlvbi1vdXRjb21lL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJleGVjdXRpb25fb3V0Y29tZV9zaGEyNTYiOiJlZjVlNWYwMzgwYmMwOWRkMjc5YjE5OTQ3Y2RhYzUyZTEzOWZmODNjYTI3NzFjZDA0NDg0MGFjNzM5MzZmMWI0IiwiZXhlY3V0aW9uX3B1cnBvc2UiOiJwcm9kdWN0aW9uIiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDNaIiwicGVuZGluZ19wZXJtaXRfY291bnQiOjAsInBlcm1pdF9jaGFpbiI6eyJlbnRyaWVzIjpbeyJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiZGVsaXZlcmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMVoiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4Iiwib25lX3VzZV9jbGFpbV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsInBlcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5IiwicGVybWl0X2lkIjoicGVybWl0OjEiLCJydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6IjhhOGY2ZDg4NzUwY2JiNmIzYmQ2NjIwOTNhZDI5ZTJiYzIwYzZhYjY5OTFkNzBlNTg4NjkxYzhiM2FiNjYyYjkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJBT0txQ2dTMXMwWGEtaXlFOTZWLXlsUHpRY3YydXZPc3V5Z0h6dnUyUnlWN0ZMc0hGX0xWZnF5eDR1MnB1bWxFbDBZazVZdTcwSlZVa3NacGFVcEREdyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwiZGVsaXZlcnlfcmVjZWlwdF9hcnRpZmFjdF9zaGEyNTYiOiJjMDBhMWJhNzAxNWYzY2JlMDVhNzZhZWFiZmY2MTlkNzkzZTczNzVhMzEwZTE5NWZjYmJkMmQyOTQ4ZjJiMjE0IiwicGVybWl0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJwZXJtaXRfaWQiOiJwZXJtaXQ6MSIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2NoZWNrZWRfYXQiOiIyMDI2LTA4LTE4VDExOjU5OjMwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2V4cGlyZXNfYXQiOiIyMDI2LTA4LTIwVDExOjAwOjAwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiJjODdmNTc1MWFhOWIyNzljY2UyM2FhMTc4YWZmOTU2ZTE5ZjY5MGZjZDJmOWY3MGUwZDk5OTc4MmE2OTBkNmI4Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiIwQnlIZ3FEeFFacm5nTElGLTZ0Wk9xOWxYYW1jQVFDMGRvVVRuZUtOUGVTLVN5b09iUXlPa0F1UEZLNVBZY056T2JaeGdzZ0FMZFdhSV9zYVVDcGxCZyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwicGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi1lbnRyeS92MiJ9XSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjUwNDhmZTk0NTg4MmQyODAyNjAwOGQ3ZTkxMzdhZDAxNmZkZDAxYWVhZjU4ODQ4MWZiNWJkMGYyNDJhNTAwNDMiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi92MiJ9LCJwZXJtaXRfY291bnQiOjEsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlY2VpcHQiOnsiYXV0aG9yaXphdGlvbl9jb25maXJtZWQiOjEsImF1dGhvcml6YXRpb25fcmVxdWlyZWQiOjEsImJ1bmRsZV9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZHVyYXRpb25fbXMiOjEwMCwiZWZmZWN0X3RpZXJfcmVhY2hlZCI6ImluZGVwZW5kZW50X3N5c3RlbSIsImVmZmVjdHNfY29uZmlybWVkIjoxLCJlZmZlY3RzX3JlcXVpcmVkIjoxLCJlc3RfY29zdF9taWNyb3VzZCI6MCwiZXZpZGVuY2VfY2xhc3NlcyI6WyJhdXRob3JpemF0aW9uIiwiZWZmZWN0X3RpZXJfMSIsImlkZW50aXR5IiwicG9zdGNvbmRpdGlvbiJdLCJleHRlcm5hbF9uZXR3b3JrX2NhbGxzIjoibm9uZSIsImdlbmVyYXRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiaGVhbHMiOjAsImlkZW50aXR5X2FwcGxpY2FibGUiOjEsImlkZW50aXR5X2FybWVkIjoxLCJpZGVudGl0eV9jb25maXJtZWQiOjEsImlkZW50aXR5X3JlcXVpcmVkIjoxLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IlZFUklGSUVEIiwib3Zlcl9oYWx0X2NvdW50IjowLCJwb3N0Y29uZGl0aW9uc19jb25maXJtZWQiOjEsInBvc3Rjb25kaXRpb25zX3JlcXVpcmVkIjoxLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjp0cnVlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJwcm92ZW5hbmNlIjoicHJvZHVjdGlvbiIsInJlY2VpcHRfYnVpbGRlcl92ZXJzaW9uIjoiMS4yLjMiLCJydW5nX2hpc3RvZ3JhbSI6eyJzdHJ1Y3R1cmFsIjoxfSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1ydW4tcmVjZWlwdC92MSIsInNvdXJjZV9yZWNlaXB0X2RpZ2VzdCI6IjI2N2NmMDU1ZjIzYmVmYmM5NzE1NzVlMDljMTBhY2Y0ODlmNDdlNzMwOWNiYWM0YjA4YjZiMDkzZjM2NGQ1OTYiLCJzb3VyY2VfcmVjZWlwdF9zaGEyNTYiOiI4ZDkwNDJjOTlmOWU0NjBlNGI0YWExY2VlZjYzMTJiMzFlOTVlODVkZTU4ZDg5Yjc0MGU2NDEzMTgxNDQ3MGNiIiwic291cmNlX3NjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnJ1bi1yZWNlaXB0L3YyIiwic3RlcHNfb2siOjEsInN0ZXBzX3RvdGFsIjoxLCJzdWJzdHJhdGUiOiJ3ZWIiLCJ0cmFuc2FjdGlvbl9vdXRjb21lIjoiVkVSSUZJRUQifSwicnVuX3JlY2VpcHRfc2hhMjU2IjoiNjM5YzMxMzY5YjM1MDQ1NGU1ZjE0MWFhODI2OWVmYjk1Njk4NmM2MGU5NzA5YWNjNmViMDllMmZiNmYwZDgzZSIsInJ1bl9yZXBvcnRfb2JqZWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW5fcmVwb3J0X29iamVjdF92ZXJzaW9uIjoidmVyc2lvbjoxIiwicnVuX3JlcG9ydF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwicnVudGltZV9zdWJzdHJhdGUiOiJ3ZWIiLCJydW50aW1lX3ZhbGlkYXRpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXRlcm1pbmFsLXZlcmlmaWNhdGlvbi92MiIsInRlbmFudF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMiIsInRlcm1pbmFsX3NlcXVlbmNlIjoxLCJ2ZXJpZmllZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIndvcmtmbG93X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwid29ya2Zsb3dfdmVyc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNCJ9LCJzaWduYXR1cmUiOiJkVlBRN2ZhLTJSaF93Y040Sjd0TzBsSXQ0bTZmdnNEZXZjYjN1WE5zRDEwZTlmUGpOLWlsSjg0WGdiMVdvZ1ROd3NSLTJFdTY2UnZzSGVRcWFfcHJDZyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZXZpZGVuY2UtcnVubmVyLWVkMjU1MTktNTY0NzVhYTc1NDYzNDc0YyIsInB1YmxpY19rZXkiOiJBNkVIdi9QT0VMNGRjTjBZNTB2QW1XZmsxakNicFExZkhkeUdaQkpWTWJnPSJ9fQ==", + "managed_result_loss": null, + "name": "verified-complete", + "payload_canonical_sha256": "ba885e032122ee1e9c616c048d32845cbbf0b27fb67141fefc0bd45cd12b2d2f", + "signature": "dVPQ7fa-2Rh_wcN4J7tO0lIt4m6fvsDevcb3uXNsD10e9fPjN-ilJ84Xgb1WogTNwsR-2Eu66RvsHeQqa_prCg", + "terminal_verification_artifact_sha256": "23ff9e0a8f6c5e1a4fc124e08ab41e42e456bda4d32cb65c24db40e8ec7b8bcc" + }, { "callback": { "artifact_bytes_source": "envelope_canonical_base64", @@ -33,6 +75,7 @@ "verifier_identity": null }, "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6IjMyY2QzNjEzYmVlOWJjNjkwNjkzYTM5NWQ5OTMzNTQ3ZjllZWI5ZjZmYWE1ZGJkYmIxNGExMDYyOThiZTQ2NWMiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiODJkYWI1NDI4ZTI1MmQxOGE1OWIwMTc3NzM5ZTAwY2FhZGI3NjJkY2Y1OWY0MDM2ZGVhNGQ3N2MzYzMyMDNjYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6ImViYjU4MzYxNzhlZGZhYzYyOWJkOWJjYTkwZWNkMDE5NTkwOTQ5NjZhMGRkNjNiYWVhN2E2MmY2ZTNiMDRmZjgiLCJyZWNvcmRzIjpbeyJhYnNlbmNlX2Jhc2lzIjoibm90X2FjdHVhdGVkIiwiYXR0ZW1wdF9zdGF0ZSI6Im5vdF9hY3R1YXRlZCIsImVmZmVjdF9jb250cmFjdF9oYXNoIjoic2hhMjU2OmFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJlZmZlY3RfdmVyaWZpZWQiOmZhbHNlLCJmaW5hbF92ZXJkaWN0IjpudWxsLCJvYnNlcnZlZF9lZmZlY3QiOiJhYnNlbnQiLCJyZWNvbmNpbGlhdGlvbl9hY3Rpb25zIjowLCJyZWNvbmNpbGlhdGlvbl9jb21wbGV0ZWQiOmZhbHNlLCJyZWNvcmRfa2luZCI6InRlcm1pbmFsX2VmZmVjdF9zdGF0ZSIsInJlc29sdmVkX2RlbGl2ZXJ5X3VuY2VydGFpbnR5IjpmYWxzZSwicmVzdWx0X2luZGV4IjowLCJ2ZXJpZmljYXRpb25fcGVyZm9ybWVkIjpmYWxzZSwidmVyaWZpY2F0aW9uX3RpZXIiOm51bGwsInZlcmlmaWVyX2lkZW50aXR5IjpudWxsfV0sInJlcXVpcmVkIjoxLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWVmZmVjdC1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiaWRlbnRpdHkiOnsiY29uZmlybWVkIjoxLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiMTM1Yzg2M2VkYzdmYWIxOGNhMjliNDkyOTNmYzI1YTFjOWJkNGRlMGU5ZWQyOGQ0Y2VjNmU4ZDBhNjc5MTIyOSIsInJlcXVpcmVkIjoxLCJyZXN1bHRzIjpbeyJtb2RlIjoic3RydWN0dXJlZCIsInJlc3VsdF9pbmRleCI6MCwic2lnbmFscyI6W10sInN0YXR1cyI6InZlcmlmaWVkIn1dLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWlkZW50aXR5LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJwb2xpY3kiOnsiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX3BvbGljeV9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZ292ZXJuZWRfcnVudGltZV9pbnB1dHNfZGlnZXN0IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiI0Zjk4OGQwMTViY2Q3Y2Q3ZTA1YmEwNjYzZDg5ZjBhOTY4ZjBkOGFjZTk5YjA5MGViYWJlZTUyYTFjNTUwNDIxIiwibWluaW11bV9lZmZlY3RfdGllciI6MSwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb2xpY3ktZXZpZGVuY2UvdjEifSwicG9zdGNvbmRpdGlvbiI6eyJjb25maXJtZWQiOjAsIm1hbmlmZXN0X3NoYTI1NiI6IjNlYTRjNjRhMzBiMGNmMTdiY2EwNGYzMjYwZTBhNDcxOGIyN2Y1MWUxZmRjMmJiODhhMTgyNjMwYTM0ZGQ1ZmYiLCJyZWNvcmRzIjpbeyJhY3Rpb25fa2luZCI6ImNsaWNrIiwiYWN0dWF0aW9uX3BhdGgiOiJndWkiLCJjb250cmFjdF9pbmRleCI6MCwiY29udHJhY3Rfa2luZCI6ImV4cGxpY2l0X3ByZWRpY2F0ZSIsImNvbnRyYWN0X3NoYTI1NiI6IjBhZGUwYjUyMGJjMDA3ODkxY2FjY2EwMDE3MTU3ODQ0MmEyYTc0Y2VmMTVmZjY0ZGRlNDVkZjA3YWZiNjRhZWIiLCJyZXN1bHRfaW5kZXgiOjAsInN0ZXBfY29udHJhY3Rfc2hhMjU2IjoiMWE1OTUzNmE0ODhiMWM0NmFhY2YwYmEzNzkzMGI4NDA2MmMxNDI3NTVmYTkzOTE1MmVmNWUyOGM0MDk0NzFlZCIsInN0ZXBfaW5kZXgiOjAsInZlcmRpY3QiOiJ1bnZlcmlmaWFibGUiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn1dLCJyZXF1aXJlZCI6MSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb3N0Y29uZGl0aW9uLWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9fSwiZXZpZGVuY2VfcnVubmVyX3NpZ25lcl9zaGEyNTYiOiI1NjQ3NWFhNzU0NjM0NzRjMDI4NWRmNWRiZjJiY2FiNzNkYTY1MTM1ODgzOWU5Yjc3NDgxYjJlYWIxMDc3MDhjIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2lnbmVyX3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJleGVjdXRpb25fb3V0Y29tZSI6eyJjb21wZW5zYXRpb25fYWN0aW9ucyI6MCwiZXZpZGVuY2VfY2xhc3NlcyI6WyJhdXRob3JpemF0aW9uIiwiaWRlbnRpdHkiXSwiZXhlY3V0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJub25lIiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJwYXNzZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjEsInBvc3Rjb25kaXRpb24iOjB9LCJwb3N0Y29uZGl0aW9uX2V2aWRlbmNlIjpbeyJhY3Rpb25fa2luZCI6ImNsaWNrIiwiYWN0dWF0aW9uX3BhdGgiOiJndWkiLCJjb250cmFjdF9pbmRleCI6MCwiY29udHJhY3Rfa2luZCI6ImV4cGxpY2l0X3ByZWRpY2F0ZSIsImNvbnRyYWN0X3NoYTI1NiI6IjBhZGUwYjUyMGJjMDA3ODkxY2FjY2EwMDE3MTU3ODQ0MmEyYTc0Y2VmMTVmZjY0ZGRlNDVkZjA3YWZiNjRhZWIiLCJyZXN1bHRfaW5kZXgiOjAsInN0ZXBfY29udHJhY3Rfc2hhMjU2IjoiMWE1OTUzNmE0ODhiMWM0NmFhY2YwYmEzNzkzMGI4NDA2MmMxNDI3NTVmYTkzOTE1MmVmNWUyOGM0MDk0NzFlZCIsInN0ZXBfaW5kZXgiOjAsInZlcmRpY3QiOiJ1bnZlcmlmaWFibGUiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn1dLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjpmYWxzZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicXVhbGlmaWNhdGlvbl9ldmlkZW5jZV9vbmx5IjpmYWxzZSwicmVxdWlyZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MSwiaWRlbnRpdHkiOjEsInBvc3Rjb25kaXRpb24iOjF9LCJ2ZXJzaW9uIjoib3BlbmFkYXB0LmV4ZWN1dGlvbi1vdXRjb21lL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJleGVjdXRpb25fb3V0Y29tZV9zaGEyNTYiOiJmZTM4OWJhYjBlYjQ0NDdjZWMxOTk4Mjc0ZjcyN2NhNTcwMDQ5ZmVmZDU1N2RiNzk5MDQ3Njk5ZjJmNzhmMTQyIiwiZXhlY3V0aW9uX3B1cnBvc2UiOiJwcm9kdWN0aW9uIiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjowLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDNaIiwicGVuZGluZ19wZXJtaXRfY291bnQiOjAsInBlcm1pdF9jaGFpbiI6eyJlbnRyaWVzIjpbXSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjgyZGFiNTQyOGUyNTJkMThhNTliMDE3NzczOWUwMGNhYWRiNzYyZGNmNTlmNDAzNmRlYTRkNzdjM2MzMjAzY2IiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi92MiJ9LCJwZXJtaXRfY291bnQiOjAsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlY2VpcHQiOnsiYXV0aG9yaXphdGlvbl9jb25maXJtZWQiOjEsImF1dGhvcml6YXRpb25fcmVxdWlyZWQiOjEsImJ1bmRsZV9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZHVyYXRpb25fbXMiOjEwMCwiZWZmZWN0X3RpZXJfcmVhY2hlZCI6Im5vbmUiLCJlZmZlY3RzX2NvbmZpcm1lZCI6MCwiZWZmZWN0c19yZXF1aXJlZCI6MSwiZXN0X2Nvc3RfbWljcm91c2QiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiIsImlkZW50aXR5Il0sImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJub25lIiwiZ2VuZXJhdGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJoZWFscyI6MCwiaWRlbnRpdHlfYXBwbGljYWJsZSI6MSwiaWRlbnRpdHlfYXJtZWQiOjEsImlkZW50aXR5X2NvbmZpcm1lZCI6MSwiaWRlbnRpdHlfcmVxdWlyZWQiOjEsIm1vZGVsX2NhbGxzIjowLCJvdXRjb21lIjoiSEFMVEVEIiwib3Zlcl9oYWx0X2NvdW50IjowLCJwb3N0Y29uZGl0aW9uc19jb25maXJtZWQiOjAsInBvc3Rjb25kaXRpb25zX3JlcXVpcmVkIjoxLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjpmYWxzZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicHJvdmVuYW5jZSI6InByb2R1Y3Rpb24iLCJyZWNlaXB0X2J1aWxkZXJfdmVyc2lvbiI6IjEuMi4zIiwicnVuZ19oaXN0b2dyYW0iOnt9LCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXJ1bi1yZWNlaXB0L3YxIiwic291cmNlX3JlY2VpcHRfZGlnZXN0IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9yZWNlaXB0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzb3VyY2Vfc2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucnVuLXJlcG9ydC92MSIsInN0ZXBzX29rIjowLCJzdGVwc190b3RhbCI6MSwic3Vic3RyYXRlIjoid2ViIiwidHJhbnNhY3Rpb25fb3V0Y29tZSI6IkhBTFRFRF9CRUZPUkVfRUZGRUNUIn0sInJ1bl9yZWNlaXB0X3NoYTI1NiI6ImFiNTBhMDA2YTg4OGIzZjkwZTFlNzFlZjIwOTIxYWY3MTdiMmY0MWEzNjc2ZTYwNWZiZGI4MjBjYjZiNTNjYzQiLCJydW5fcmVwb3J0X29iamVjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVuX3JlcG9ydF9vYmplY3RfdmVyc2lvbiI6InZlcnNpb246aGFsdGVkOjEiLCJydW5fcmVwb3J0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW50aW1lX3N1YnN0cmF0ZSI6IndlYiIsInJ1bnRpbWVfdmFsaWRhdGlvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfc2VxdWVuY2UiOjEsInZlcmlmaWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwid29ya2Zsb3dfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJ3b3JrZmxvd192ZXJzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA0In0sInNpZ25hdHVyZSI6IktHQzRqQllpN01ReVlkbFV5TnJ5bGowcmwxaUVNbUFFeHQwUmlxWjV2bGh0Y1lWQzVJOEY1RDZ2V0NkRVlnb09takZNOU5UNjZmVW95ODVsYlFOQkRnIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJldmlkZW5jZS1ydW5uZXItZWQyNTUxOS01NjQ3NWFhNzU0NjM0NzRjIiwicHVibGljX2tleSI6IkE2RUh2L1BPRUw0ZGNOMFk1MHZBbVdmazFqQ2JwUTFmSGR5R1pCSlZNYmc9In19", + "managed_result_loss": null, "name": "halted-before-effect-zero-permit", "payload_canonical_sha256": "a2909800fffc776ac70a5f6beb77b7c9941a91e7e2930eb20d9042210dc66001", "signature": "KGC4jBYi7MQyYdlUyNrylj0rl1iEMmAExt0RiqZ5vlhtcYVC5I8F5D6vWCdEYgoOmjFM9NT66fUoy85lbQNBDg", @@ -65,10 +108,142 @@ "verifier_identity": null }, "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6IjE1YzBlNzZkYzJlMzIxYTM4NGU1M2VjMTVmZjUzZTc1NTU3YjdjZGU3MWM1YWQyYmEyYjkzZmYyZTk1ODM1YTUiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTliMzk5NzFlMzA1MTA5YzU0Mjk5NDVhNWZhYjQ2NjBiMDlmNDFlN2FjM2ZmZDkyZTM1MDZhZjAwMzM3ZGE5NSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6ImFmMTUwOWNjMmU5ZmExZjViOGI4MTVlN2IxZjYwNDYxOWIyYTM1ZWMwZGU1ZTBkMzRiYmZhNjdjYzkxZGJiYzciLCJyZWNvcmRzIjpbeyJhYnNlbmNlX2Jhc2lzIjoibm9uZSIsImF0dGVtcHRfc3RhdGUiOiJkZWxpdmVyeV91bmNlcnRhaW4iLCJlZmZlY3RfY29udHJhY3RfaGFzaCI6InNoYTI1NjphYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X3ZlcmlmaWVkIjpmYWxzZSwiZmluYWxfdmVyZGljdCI6bnVsbCwib2JzZXJ2ZWRfZWZmZWN0IjoidW5rbm93biIsInJlY29uY2lsaWF0aW9uX2FjdGlvbnMiOjAsInJlY29uY2lsaWF0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsInJlY29yZF9raW5kIjoidGVybWluYWxfZWZmZWN0X3N0YXRlIiwicmVzb2x2ZWRfZGVsaXZlcnlfdW5jZXJ0YWludHkiOmZhbHNlLCJyZXN1bHRfaW5kZXgiOjAsInZlcmlmaWNhdGlvbl9wZXJmb3JtZWQiOmZhbHNlLCJ2ZXJpZmljYXRpb25fdGllciI6bnVsbCwidmVyaWZpZXJfaWRlbnRpdHkiOm51bGx9XSwicmVxdWlyZWQiOjEsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZWZmZWN0LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJpZGVudGl0eSI6eyJjb25maXJtZWQiOjEsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiIxMzVjODYzZWRjN2ZhYjE4Y2EyOWI0OTI5M2ZjMjVhMWM5YmQ0ZGUwZTllZDI4ZDRjZWM2ZThkMGE2NzkxMjI5IiwicmVxdWlyZWQiOjEsInJlc3VsdHMiOlt7Im1vZGUiOiJzdHJ1Y3R1cmVkIiwicmVzdWx0X2luZGV4IjowLCJzaWduYWxzIjpbXSwic3RhdHVzIjoidmVyaWZpZWQifV0sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24taWRlbnRpdHktZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sInBvbGljeSI6eyJhZG1pc3Npb25fcG9saWN5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZ292ZXJuZWRfcG9saWN5X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJnb3Zlcm5lZF9ydW50aW1lX2lucHV0c19kaWdlc3QiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsIm1hbmlmZXN0X3NoYTI1NiI6IjRmOTg4ZDAxNWJjZDdjZDdlMDViYTA2NjNkODlmMGE5NjhmMGQ4YWNlOTliMDkwZWJhYmVlNTJhMWM1NTA0MjEiLCJtaW5pbXVtX2VmZmVjdF90aWVyIjoxLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvbGljeS1ldmlkZW5jZS92MSJ9LCJwb3N0Y29uZGl0aW9uIjp7ImNvbmZpcm1lZCI6MCwibWFuaWZlc3Rfc2hhMjU2IjoiM2VhNGM2NGEzMGIwY2YxN2JjYTA0ZjMyNjBlMGE0NzE4YjI3ZjUxZTFmZGMyYmI4OGExODI2MzBhMzRkZDVmZiIsInJlY29yZHMiOlt7ImFjdGlvbl9raW5kIjoiY2xpY2siLCJhY3R1YXRpb25fcGF0aCI6Imd1aSIsImNvbnRyYWN0X2luZGV4IjowLCJjb250cmFjdF9raW5kIjoiZXhwbGljaXRfcHJlZGljYXRlIiwiY29udHJhY3Rfc2hhMjU2IjoiMGFkZTBiNTIwYmMwMDc4OTFjYWNjYTAwMTcxNTc4NDQyYTJhNzRjZWYxNWZmNjRkZGU0NWRmMDdhZmI2NGFlYiIsInJlc3VsdF9pbmRleCI6MCwic3RlcF9jb250cmFjdF9zaGEyNTYiOiIxYTU5NTM2YTQ4OGIxYzQ2YWFjZjBiYTM3OTMwYjg0MDYyYzE0Mjc1NWZhOTM5MTUyZWY1ZTI4YzQwOTQ3MWVkIiwic3RlcF9pbmRleCI6MCwidmVyZGljdCI6InVudmVyaWZpYWJsZSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifV0sInJlcXVpcmVkIjoxLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvc3Rjb25kaXRpb24tZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn19LCJldmlkZW5jZV9ydW5uZXJfc2lnbmVyX3NoYTI1NiI6IjU2NDc1YWE3NTQ2MzQ3NGMwMjg1ZGY1ZGJmMmJjYWI3M2RhNjUxMzU4ODM5ZTliNzc0ODFiMmVhYjEwNzcwOGMiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImV4ZWN1dGlvbl9vdXRjb21lIjp7ImNvbXBlbnNhdGlvbl9hY3Rpb25zIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iLCJpZGVudGl0eSJdLCJleGVjdXRpb25fY29tcGxldGVkIjpmYWxzZSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsInBhc3NlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjowLCJpZGVudGl0eSI6MSwicG9zdGNvbmRpdGlvbiI6MH0sInBvc3Rjb25kaXRpb25fZXZpZGVuY2UiOlt7ImFjdGlvbl9raW5kIjoiY2xpY2siLCJhY3R1YXRpb25fcGF0aCI6Imd1aSIsImNvbnRyYWN0X2luZGV4IjowLCJjb250cmFjdF9raW5kIjoiZXhwbGljaXRfcHJlZGljYXRlIiwiY29udHJhY3Rfc2hhMjU2IjoiMGFkZTBiNTIwYmMwMDc4OTFjYWNjYTAwMTcxNTc4NDQyYTJhNzRjZWYxNWZmNjRkZGU0NWRmMDdhZmI2NGFlYiIsInJlc3VsdF9pbmRleCI6MCwic3RlcF9jb250cmFjdF9zaGEyNTYiOiIxYTU5NTM2YTQ4OGIxYzQ2YWFjZjBiYTM3OTMwYjg0MDYyYzE0Mjc1NWZhOTM5MTUyZWY1ZTI4YzQwOTQ3MWVkIiwic3RlcF9pbmRleCI6MCwidmVyZGljdCI6InVudmVyaWZpYWJsZSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifV0sInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJxdWFsaWZpY2F0aW9uX2V2aWRlbmNlX29ubHkiOmZhbHNlLCJyZXF1aXJlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjoxLCJpZGVudGl0eSI6MSwicG9zdGNvbmRpdGlvbiI6MX0sInZlcnNpb24iOiJvcGVuYWRhcHQuZXhlY3V0aW9uLW91dGNvbWUvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sImV4ZWN1dGlvbl9vdXRjb21lX3NoYTI1NiI6ImZlMzg5YmFiMGViNDQ0N2NlYzE5OTgyNzRmNzI3Y2E1NzAwNDlmZWZkNTU3ZGI3OTkwNDc2OTlmMmY3OGYxNDIiLCJleGVjdXRpb25fcHVycG9zZSI6InByb2R1Y3Rpb24iLCJmaW5hbF9hdXRob3JpdHlfc2VxdWVuY2UiOjAsImZpbmFsX3J1bnRpbWVfZGVsaXZlcnlfc2VxdWVuY2UiOjAsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowM1oiLCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MSwicGVybWl0X2NoYWluIjp7ImVudHJpZXMiOltdLCJwZW5kaW5nIjp7ImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3QiOm51bGwsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3Rfc2hhMjU2IjpudWxsLCJkZWxpdmVyeV9zdGF0ZSI6IlVOUkVTT0xWRUQiLCJwZXJtaXRfYXJ0aWZhY3QiOnsicGF5bG9hZCI6eyJhY3Rpb25fcmVxdWVzdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2FydGlmYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJhdXRob3JpdHlfc2VxdWVuY2UiOjAsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJldmlkZW5jZV9pZGVudGl0eV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsInBlcm1pdF9pZCI6InBlcm1pdDoxIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfY2hlY2tlZF9hdCI6IjIwMjYtMDgtMThUMTE6NTk6MzBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfZXhwaXJlc19hdCI6IjIwMjYtMDgtMjBUMTE6MDA6MDBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6ImM4N2Y1NzUxYWE5YjI3OWNjZTIzYWExNzhhZmY5NTZlMTlmNjkwZmNkMmY5ZjcwZTBkOTk5NzgyYTY5MGQ2YjgiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6IjBCeUhncUR4UVpybmdMSUYtNnRaT3E5bFhhbWNBUUMwZG9VVG5lS05QZVMtU3lvT2JReU9rQXVQRks1UFljTnpPYlp4Z3NnQUxkV2FJX3NhVUNwbEJnIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJwZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZW5kaW5nLXBlcm1pdC92MiJ9LCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTliMzk5NzFlMzA1MTA5YzU0Mjk5NDVhNWZhYjQ2NjBiMDlmNDFlN2FjM2ZmZDkyZTM1MDZhZjAwMzM3ZGE5NSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWNoYWluL3YyIn0sInBlcm1pdF9jb3VudCI6MSwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVjZWlwdCI6eyJhdXRob3JpemF0aW9uX2NvbmZpcm1lZCI6MSwiYXV0aG9yaXphdGlvbl9yZXF1aXJlZCI6MSwiYnVuZGxlX2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJkdXJhdGlvbl9tcyI6MTAwLCJlZmZlY3RfdGllcl9yZWFjaGVkIjoibm9uZSIsImVmZmVjdHNfY29uZmlybWVkIjowLCJlZmZlY3RzX3JlcXVpcmVkIjoxLCJlc3RfY29zdF9taWNyb3VzZCI6MCwiZXZpZGVuY2VfY2xhc3NlcyI6WyJhdXRob3JpemF0aW9uIiwiaWRlbnRpdHkiXSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJnZW5lcmF0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImhlYWxzIjowLCJpZGVudGl0eV9hcHBsaWNhYmxlIjoxLCJpZGVudGl0eV9hcm1lZCI6MSwiaWRlbnRpdHlfY29uZmlybWVkIjoxLCJpZGVudGl0eV9yZXF1aXJlZCI6MSwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJvdmVyX2hhbHRfY291bnQiOjAsInBvc3Rjb25kaXRpb25zX2NvbmZpcm1lZCI6MCwicG9zdGNvbmRpdGlvbnNfcmVxdWlyZWQiOjEsInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJwcm92ZW5hbmNlIjoicHJvZHVjdGlvbiIsInJlY2VpcHRfYnVpbGRlcl92ZXJzaW9uIjoiMS4yLjMiLCJydW5nX2hpc3RvZ3JhbSI6e30sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcnVuLXJlY2VpcHQvdjEiLCJzb3VyY2VfcmVjZWlwdF9kaWdlc3QiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3JlY2VpcHRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9zY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5ydW4tcmVwb3J0L3YxIiwic3RlcHNfb2siOjAsInN0ZXBzX3RvdGFsIjoxLCJzdWJzdHJhdGUiOiJ3ZWIiLCJ0cmFuc2FjdGlvbl9vdXRjb21lIjoiUkVDT05DSUxJQVRJT05fUkVRVUlSRUQifSwicnVuX3JlY2VpcHRfc2hhMjU2IjoiMThlYjJjNGY0MTUzZGFiMDUzYmYwMjdjMjZmNjhhZmEwNjEzMTdlM2MyZTdkNWE2MGVlNThjM2VlNmIzZDdjNiIsInJ1bl9yZXBvcnRfb2JqZWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW5fcmVwb3J0X29iamVjdF92ZXJzaW9uIjoidmVyc2lvbjpyZWNvbmNpbGlhdGlvbjoxIiwicnVuX3JlcG9ydF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwicnVudGltZV9zdWJzdHJhdGUiOiJ3ZWIiLCJydW50aW1lX3ZhbGlkYXRpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXRlcm1pbmFsLXZlcmlmaWNhdGlvbi92MiIsInRlbmFudF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMiIsInRlcm1pbmFsX3NlcXVlbmNlIjoxLCJ2ZXJpZmllZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIndvcmtmbG93X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwid29ya2Zsb3dfdmVyc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNCJ9LCJzaWduYXR1cmUiOiJkRUdXZ0NDTGZGTm1QNnV5X0JuZ1dZUHJnel9ScmpYeFVULUpkSEwzaTNEclZSTnJ6OWJsWlpzbTBRQXo2cWVHTy1UZkhHT2FLMTVTdUc2ZlIwRGpCdyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZXZpZGVuY2UtcnVubmVyLWVkMjU1MTktNTY0NzVhYTc1NDYzNDc0YyIsInB1YmxpY19rZXkiOiJBNkVIdi9QT0VMNGRjTjBZNTB2QW1XZmsxakNicFExZkhkeUdaQkpWTWJnPSJ9fQ==", + "managed_result_loss": null, "name": "reconciliation-required-pending-permit", "payload_canonical_sha256": "2f2af3926539d2bde7a12888bddbdc26aa3193818b82a01743a100d5181581b0", "signature": "dEGWgCCLfFNmP6uy_BngWYPrgz_RrjXxUT-JdHL3i3DrVRNrz9blZZsm0QAz6qeGO-TfHGOaK15SuG6fR0DjBw", "terminal_verification_artifact_sha256": "2abcc6c65e0288a235b901169415d59632c8eaacf9b1aac087b1bb0d55ced624" + }, + { + "callback": { + "artifact_bytes_source": "envelope_canonical_base64", + "outcome": "RECONCILIATION_REQUIRED", + "report_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "run_id": "00000000-0000-4000-8000-000000000001", + "schema_version": "openadapt.hosted-runner-terminal/v1", + "started": true, + "uncertain_delivery": false + }, + "effect_state": { + "absence_basis": "none", + "attempt_state": "delivery_uncertain", + "effect_contract_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "effect_verified": false, + "final_verdict": null, + "observed_effect": "unknown", + "reconciliation_actions": 0, + "reconciliation_completed": false, + "record_kind": "terminal_effect_state", + "resolved_delivery_uncertainty": false, + "result_index": 0, + "verification_performed": false, + "verification_tier": null, + "verifier_identity": null + }, + "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjEsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6ImYwY2ExOWE2YjNlYWJlNTk0ZGJjOTA2ODljMmY3NGNiZTVlODJkNGQ5NDdkYjI5YTVkMDdiMTY0MWU4OTYxY2YiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6ImFmMTUwOWNjMmU5ZmExZjViOGI4MTVlN2IxZjYwNDYxOWIyYTM1ZWMwZGU1ZTBkMzRiYmZhNjdjYzkxZGJiYzciLCJyZWNvcmRzIjpbeyJhYnNlbmNlX2Jhc2lzIjoibm9uZSIsImF0dGVtcHRfc3RhdGUiOiJkZWxpdmVyeV91bmNlcnRhaW4iLCJlZmZlY3RfY29udHJhY3RfaGFzaCI6InNoYTI1NjphYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X3ZlcmlmaWVkIjpmYWxzZSwiZmluYWxfdmVyZGljdCI6bnVsbCwib2JzZXJ2ZWRfZWZmZWN0IjoidW5rbm93biIsInJlY29uY2lsaWF0aW9uX2FjdGlvbnMiOjAsInJlY29uY2lsaWF0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsInJlY29yZF9raW5kIjoidGVybWluYWxfZWZmZWN0X3N0YXRlIiwicmVzb2x2ZWRfZGVsaXZlcnlfdW5jZXJ0YWludHkiOmZhbHNlLCJyZXN1bHRfaW5kZXgiOjAsInZlcmlmaWNhdGlvbl9wZXJmb3JtZWQiOmZhbHNlLCJ2ZXJpZmljYXRpb25fdGllciI6bnVsbCwidmVyaWZpZXJfaWRlbnRpdHkiOm51bGx9XSwicmVxdWlyZWQiOjEsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZWZmZWN0LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJpZGVudGl0eSI6eyJjb25maXJtZWQiOjEsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiIxMzVjODYzZWRjN2ZhYjE4Y2EyOWI0OTI5M2ZjMjVhMWM5YmQ0ZGUwZTllZDI4ZDRjZWM2ZThkMGE2NzkxMjI5IiwicmVxdWlyZWQiOjEsInJlc3VsdHMiOlt7Im1vZGUiOiJzdHJ1Y3R1cmVkIiwicmVzdWx0X2luZGV4IjowLCJzaWduYWxzIjpbXSwic3RhdHVzIjoidmVyaWZpZWQifV0sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24taWRlbnRpdHktZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sInBvbGljeSI6eyJhZG1pc3Npb25fcG9saWN5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZ292ZXJuZWRfcG9saWN5X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJnb3Zlcm5lZF9ydW50aW1lX2lucHV0c19kaWdlc3QiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsIm1hbmlmZXN0X3NoYTI1NiI6IjRmOTg4ZDAxNWJjZDdjZDdlMDViYTA2NjNkODlmMGE5NjhmMGQ4YWNlOTliMDkwZWJhYmVlNTJhMWM1NTA0MjEiLCJtaW5pbXVtX2VmZmVjdF90aWVyIjoxLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvbGljeS1ldmlkZW5jZS92MSJ9LCJwb3N0Y29uZGl0aW9uIjp7ImNvbmZpcm1lZCI6MCwibWFuaWZlc3Rfc2hhMjU2IjoiM2VhNGM2NGEzMGIwY2YxN2JjYTA0ZjMyNjBlMGE0NzE4YjI3ZjUxZTFmZGMyYmI4OGExODI2MzBhMzRkZDVmZiIsInJlY29yZHMiOlt7ImFjdGlvbl9raW5kIjoiY2xpY2siLCJhY3R1YXRpb25fcGF0aCI6Imd1aSIsImNvbnRyYWN0X2luZGV4IjowLCJjb250cmFjdF9raW5kIjoiZXhwbGljaXRfcHJlZGljYXRlIiwiY29udHJhY3Rfc2hhMjU2IjoiMGFkZTBiNTIwYmMwMDc4OTFjYWNjYTAwMTcxNTc4NDQyYTJhNzRjZWYxNWZmNjRkZGU0NWRmMDdhZmI2NGFlYiIsInJlc3VsdF9pbmRleCI6MCwic3RlcF9jb250cmFjdF9zaGEyNTYiOiIxYTU5NTM2YTQ4OGIxYzQ2YWFjZjBiYTM3OTMwYjg0MDYyYzE0Mjc1NWZhOTM5MTUyZWY1ZTI4YzQwOTQ3MWVkIiwic3RlcF9pbmRleCI6MCwidmVyZGljdCI6InVudmVyaWZpYWJsZSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifV0sInJlcXVpcmVkIjoxLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvc3Rjb25kaXRpb24tZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn19LCJldmlkZW5jZV9ydW5uZXJfc2lnbmVyX3NoYTI1NiI6IjU2NDc1YWE3NTQ2MzQ3NGMwMjg1ZGY1ZGJmMmJjYWI3M2RhNjUxMzU4ODM5ZTliNzc0ODFiMmVhYjEwNzcwOGMiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImV4ZWN1dGlvbl9vdXRjb21lIjp7ImNvbXBlbnNhdGlvbl9hY3Rpb25zIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iLCJpZGVudGl0eSJdLCJleGVjdXRpb25fY29tcGxldGVkIjpmYWxzZSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsInBhc3NlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjowLCJpZGVudGl0eSI6MSwicG9zdGNvbmRpdGlvbiI6MH0sInBvc3Rjb25kaXRpb25fZXZpZGVuY2UiOlt7ImFjdGlvbl9raW5kIjoiY2xpY2siLCJhY3R1YXRpb25fcGF0aCI6Imd1aSIsImNvbnRyYWN0X2luZGV4IjowLCJjb250cmFjdF9raW5kIjoiZXhwbGljaXRfcHJlZGljYXRlIiwiY29udHJhY3Rfc2hhMjU2IjoiMGFkZTBiNTIwYmMwMDc4OTFjYWNjYTAwMTcxNTc4NDQyYTJhNzRjZWYxNWZmNjRkZGU0NWRmMDdhZmI2NGFlYiIsInJlc3VsdF9pbmRleCI6MCwic3RlcF9jb250cmFjdF9zaGEyNTYiOiIxYTU5NTM2YTQ4OGIxYzQ2YWFjZjBiYTM3OTMwYjg0MDYyYzE0Mjc1NWZhOTM5MTUyZWY1ZTI4YzQwOTQ3MWVkIiwic3RlcF9pbmRleCI6MCwidmVyZGljdCI6InVudmVyaWZpYWJsZSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifV0sInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJxdWFsaWZpY2F0aW9uX2V2aWRlbmNlX29ubHkiOmZhbHNlLCJyZXF1aXJlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjoxLCJpZGVudGl0eSI6MSwicG9zdGNvbmRpdGlvbiI6MX0sInZlcnNpb24iOiJvcGVuYWRhcHQuZXhlY3V0aW9uLW91dGNvbWUvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sImV4ZWN1dGlvbl9vdXRjb21lX3NoYTI1NiI6ImZlMzg5YmFiMGViNDQ0N2NlYzE5OTgyNzRmNzI3Y2E1NzAwNDlmZWZkNTU3ZGI3OTkwNDc2OTlmMmY3OGYxNDIiLCJleGVjdXRpb25fcHVycG9zZSI6InByb2R1Y3Rpb24iLCJmaW5hbF9hdXRob3JpdHlfc2VxdWVuY2UiOjAsImZpbmFsX3J1bnRpbWVfZGVsaXZlcnlfc2VxdWVuY2UiOjksImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowM1oiLCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MCwicGVybWl0X2NoYWluIjp7ImVudHJpZXMiOlt7ImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3QiOnsicGF5bG9hZCI6eyJhdXRoZW50aWNhdGVkX3J1bm5lcl9pZF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiYXV0aGVudGljYXRlZF9zZXNzaW9uX2lkX3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJkZWxpdmVyZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAxWiIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJvbmVfdXNlX2NsYWltX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwicGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJwZXJtaXRfaWQiOiJwZXJtaXQ6MSIsInJ1bnRpbWVfZGVsaXZlcnlfc2VxdWVuY2UiOjksInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVjZWlwdC1wYXlsb2FkL3YyIn0sInBheWxvYWRfc2hhMjU2IjoiOGE4ZjZkODg3NTBjYmI2YjNiZDY2MjA5M2FkMjllMmJjMjBjNmFiNjk5MWQ3MGU1ODg2OTFjOGIzYWI2NjJiOSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVjZWlwdC1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6IkFPS3FDZ1MxczBYYS1peUU5NlYteWxQelFjdjJ1dk9zdXlnSHp2dTJSeVY3RkxzSEZfTFZmcXl4NHUycHVtbEVsMFlrNVl1NzBKVlVrc1pwYVVwRER3Iiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0X3NoYTI1NiI6ImMwMGExYmE3MDE1ZjNjYmUwNWE3NmFlYWJmZjYxOWQ3OTNlNzM3NWEzMTBlMTk1ZmNiYmQyZDI5NDhmMmIyMTQiLCJwZXJtaXRfYXJ0aWZhY3QiOnsicGF5bG9hZCI6eyJhY3Rpb25fcmVxdWVzdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2FydGlmYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJhdXRob3JpdHlfc2VxdWVuY2UiOjAsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJldmlkZW5jZV9pZGVudGl0eV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsInBlcm1pdF9pZCI6InBlcm1pdDoxIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfY2hlY2tlZF9hdCI6IjIwMjYtMDgtMThUMTE6NTk6MzBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfZXhwaXJlc19hdCI6IjIwMjYtMDgtMjBUMTE6MDA6MDBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6ImM4N2Y1NzUxYWE5YjI3OWNjZTIzYWExNzhhZmY5NTZlMTlmNjkwZmNkMmY5ZjcwZTBkOTk5NzgyYTY5MGQ2YjgiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6IjBCeUhncUR4UVpybmdMSUYtNnRaT3E5bFhhbWNBUUMwZG9VVG5lS05QZVMtU3lvT2JReU9rQXVQRks1UFljTnpPYlp4Z3NnQUxkV2FJX3NhVUNwbEJnIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJwZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWNoYWluLWVudHJ5L3YyIn1dLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWNoYWluL3YyIn0sInBlcm1pdF9jb3VudCI6MSwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVjZWlwdCI6eyJhdXRob3JpemF0aW9uX2NvbmZpcm1lZCI6MSwiYXV0aG9yaXphdGlvbl9yZXF1aXJlZCI6MSwiYnVuZGxlX2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJkdXJhdGlvbl9tcyI6MTAwLCJlZmZlY3RfdGllcl9yZWFjaGVkIjoibm9uZSIsImVmZmVjdHNfY29uZmlybWVkIjowLCJlZmZlY3RzX3JlcXVpcmVkIjoxLCJlc3RfY29zdF9taWNyb3VzZCI6MCwiZXZpZGVuY2VfY2xhc3NlcyI6WyJhdXRob3JpemF0aW9uIiwiaWRlbnRpdHkiXSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJnZW5lcmF0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImhlYWxzIjowLCJpZGVudGl0eV9hcHBsaWNhYmxlIjoxLCJpZGVudGl0eV9hcm1lZCI6MSwiaWRlbnRpdHlfY29uZmlybWVkIjoxLCJpZGVudGl0eV9yZXF1aXJlZCI6MSwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJvdmVyX2hhbHRfY291bnQiOjAsInBvc3Rjb25kaXRpb25zX2NvbmZpcm1lZCI6MCwicG9zdGNvbmRpdGlvbnNfcmVxdWlyZWQiOjEsInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJwcm92ZW5hbmNlIjoicHJvZHVjdGlvbiIsInJlY2VpcHRfYnVpbGRlcl92ZXJzaW9uIjoiMS4yLjMiLCJydW5nX2hpc3RvZ3JhbSI6e30sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcnVuLXJlY2VpcHQvdjEiLCJzb3VyY2VfcmVjZWlwdF9kaWdlc3QiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3JlY2VpcHRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9zY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5ydW4tcmVwb3J0L3YxIiwic3RlcHNfb2siOjAsInN0ZXBzX3RvdGFsIjoxLCJzdWJzdHJhdGUiOiJ3ZWIiLCJ0cmFuc2FjdGlvbl9vdXRjb21lIjoiUkVDT05DSUxJQVRJT05fUkVRVUlSRUQifSwicnVuX3JlY2VpcHRfc2hhMjU2IjoiMThlYjJjNGY0MTUzZGFiMDUzYmYwMjdjMjZmNjhhZmEwNjEzMTdlM2MyZTdkNWE2MGVlNThjM2VlNmIzZDdjNiIsInJ1bl9yZXBvcnRfb2JqZWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW5fcmVwb3J0X29iamVjdF92ZXJzaW9uIjoidmVyc2lvbjpyZWNvbmNpbGlhdGlvbjphY2tub3dsZWRnZWQ6MSIsInJ1bl9yZXBvcnRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bnRpbWVfc3Vic3RyYXRlIjoid2ViIiwicnVudGltZV92YWxpZGF0aW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA1Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi10ZXJtaW5hbC12ZXJpZmljYXRpb24vdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9zZXF1ZW5jZSI6MSwidmVyaWZpZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJ3b3JrZmxvd19pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMyIsIndvcmtmbG93X3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQifSwic2lnbmF0dXJlIjoiQjVjRnVWbl81dkhLWFRES1FjYWtTakJ1NFRxd2lMblFrRG50MXU2VERhM1dJTTk5bXRQTTVfdW9WdmJka056NnhtbDZYdDljWHpITW1IbkYyZkh3Q1EiLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImV2aWRlbmNlLXJ1bm5lci1lZDI1NTE5LTU2NDc1YWE3NTQ2MzQ3NGMiLCJwdWJsaWNfa2V5IjoiQTZFSHYvUE9FTDRkY04wWTUwdkFtV2ZrMWpDYnBRMWZIZHlHWkJKVk1iZz0ifX0=", + "managed_result_loss": null, + "name": "reconciliation-required-acknowledged-inconclusive", + "payload_canonical_sha256": "fb4579f7c07f9cd2a46ed8af3a040fc7e06883401be7e10466ef9bed4324ca68", + "signature": "B5cFuVn_5vHKXTDKQcakSjBu4TqwiLnQkDnt1u6TDa3WIM99mtPM5_uoVvbdkNz6xml6Xt9cXzHMmHnF2fHwCQ", + "terminal_verification_artifact_sha256": "90cf0455a33f17c0462915a89dee93a3fb8a81f6a59cedbef1de2d709277cade" + }, + { + "callback": { + "artifact_bytes_source": "envelope_canonical_base64", + "outcome": "RECONCILIATION_REQUIRED", + "report_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "run_id": "00000000-0000-4000-8000-000000000001", + "schema_version": "openadapt.hosted-runner-terminal/v1", + "started": true, + "uncertain_delivery": true + }, + "effect_state": null, + "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlIjp7InBheWxvYWQiOnsiYWNrbm93bGVkZ2VkX3Blcm1pdF9jb3VudCI6MCwiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6IjY1YTZiZWQ5MjNlNDVhNmFhMGM2MjQzMDQ1ZTAzNjRjOTBhY2UwNjQ4ZmVjYjViMGJiMmViZDk0MDI3OTNjNzciLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJjbG9zZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsImNsb3N1cmVfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTEiLCJjbG9zdXJlX3JlcXVlc3Rfc2hhMjU2IjoiYmFmNDE3YWU0OGVlYzQwNjU4NGVmNTI4NmFhYTFiMWFjMmU2ZTAzZDhhMjlhNGZiNGIwYzUzMjcwNGJlNTljNiIsImNsb3N1cmVfc2VxdWVuY2UiOjEsImRlbGl2ZXJ5X2Fja25vd2xlZGdtZW50X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9pbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjowLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbXBvdGVuY3lfa2V5X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJuZXdfcGVybWl0X2F1dGhvcml6ZWQiOmZhbHNlLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsInBlbmRpbmdfYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsInBlbmRpbmdfcGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJwZXJtaXRfY291bnQiOjEsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicmVzdWx0X2xvc3Nfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW5fc3RvcmVfaWRlbnRpdHlfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1wYXlsb2FkL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfY2FsbGJhY2tfcmVxdWlyZWQiOnRydWV9LCJwYXlsb2FkX3NoYTI1NiI6IjhkYjU4ZjZhMDVmNGVmMWY4Mjc3NjYwMGQ1ZTJhMTMwNjQ1YTNlMTJiNTBmZWU2ODEwODg3NGVmZjA4ZjdlYmYiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJhakNhd3h3WTFZaUFVMXdwM2t1cFlHa3pubDcyVkFUcGxIV2dZMXVYNTdqRU9Gal80czJfdlhQbFVFQXlVbXBOaHA0eG9xVFhCUDhjOE9lMkpMZTNEdyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV2aWRlbmNlX2lkZW50aXR5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJldmlkZW5jZV9tYW5pZmVzdHMiOnsiYXV0aG9yaXphdGlvbiI6eyJhZG1pc3Npb25fYXJ0aWZhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNiIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJnb3Zlcm5lZF9hdXRob3JpemF0aW9uX2lkX3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJtYW5pZmVzdF9zaGEyNTYiOiIxNWMwZTc2ZGMyZTMyMWEzODRlNTNlYzE1ZmY1M2U3NTU1N2I3Y2RlNzFjNWFkMmJhMmI5M2ZmMmU5NTgzNWE1IiwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWF1dGhvcml6YXRpb24tZXZpZGVuY2UvdjEifSwiZWZmZWN0Ijp7ImNvbmZpcm1lZCI6MCwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJtYW5pZmVzdF9zaGEyNTYiOiI3ZTEyZGNhNWM0NGNmOTk4YTQ2OTM2MmI4NjJlODE2YTdkMmM4NTQxNjNmNjNlOGRlNjFmYjc5ZTk1YmE4MDA3IiwicmVjb3JkcyI6W10sInJlcXVpcmVkIjowLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWVmZmVjdC1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiaWRlbnRpdHkiOnsiY29uZmlybWVkIjowLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiZDNhN2VhYzM0MjdkNzZmNmJjMzYwMzA3Mzg2MDkxMTlmMDdlMGQ5MWE2ZDM1OGFiY2U1YjZmMGQ2ZGY3ODY0NSIsInJlcXVpcmVkIjowLCJyZXN1bHRzIjpbXSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1pZGVudGl0eS1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwicG9saWN5Ijp7ImFkbWlzc2lvbl9wb2xpY3lfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImVmZmVjdF9jb250cmFjdF9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZW52aXJvbm1lbnRfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJnb3Zlcm5lZF9wb2xpY3lfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImdvdmVybmVkX3J1bnRpbWVfaW5wdXRzX2RpZ2VzdCI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiNGY5ODhkMDE1YmNkN2NkN2UwNWJhMDY2M2Q4OWYwYTk2OGYwZDhhY2U5OWIwOTBlYmFiZWU1MmExYzU1MDQyMSIsIm1pbmltdW1fZWZmZWN0X3RpZXIiOjEsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9saWN5LWV2aWRlbmNlL3YxIn0sInBvc3Rjb25kaXRpb24iOnsiY29uZmlybWVkIjowLCJtYW5pZmVzdF9zaGEyNTYiOiI1MmYwOWQ1ZDYzOWIxYzM4M2YzNzc5NjNlNDg3Y2I4OWQ4OTQxNzg1N2U0NGRhN2UzZjIzZTU1YmU3NTFlN2Y0IiwicmVjb3JkcyI6W10sInJlcXVpcmVkIjowLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvc3Rjb25kaXRpb24tZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn19LCJldmlkZW5jZV9ydW5uZXJfc2lnbmVyX3NoYTI1NiI6IjU2NDc1YWE3NTQ2MzQ3NGMwMjg1ZGY1ZGJmMmJjYWI3M2RhNjUxMzU4ODM5ZTliNzc0ODFiMmVhYjEwNzcwOGMiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImV4ZWN1dGlvbl9vdXRjb21lIjp7ImNvbXBlbnNhdGlvbl9hY3Rpb25zIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iXSwiZXhlY3V0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJub25lIiwibWFuYWdlZF9yZXN1bHRfbG9zc19ldmlkZW5jZV9zaGEyNTYiOiIzODEzMTljMWQ3NjJhMjNjMjExNmEyZWUwNzQxNzZjMDZmOWM4NDIwYjk1MTM4ZWFkYjliNjdkYTZhM2I0NDQ3IiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJwYXNzZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjAsInBvc3Rjb25kaXRpb24iOjB9LCJwb3N0Y29uZGl0aW9uX2V2aWRlbmNlIjpbXSwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInF1YWxpZmljYXRpb25fZXZpZGVuY2Vfb25seSI6ZmFsc2UsInJlcXVpcmVkX2NvbnRyYWN0cyI6eyJhdXRob3JpemF0aW9uIjoxLCJlZmZlY3QiOjAsImlkZW50aXR5IjowLCJwb3N0Y29uZGl0aW9uIjowfSwidmVyc2lvbiI6Im9wZW5hZGFwdC5leGVjdXRpb24tb3V0Y29tZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiZXhlY3V0aW9uX291dGNvbWVfc2hhMjU2IjoiNzBjZjZmODE3NzJiMGJlNGIxZDcwNjdhMDVhYjRjMjkzMzY5MjM2ZDAyOWFhMWJlOGQ3OWQ2YzdjNzMxM2VmNSIsImV4ZWN1dGlvbl9wdXJwb3NlIjoicHJvZHVjdGlvbiIsImZpbmFsX2F1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZmluYWxfcnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6MCwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAzWiIsIm1hbmFnZWRfcmVzdWx0X2xvc3MiOnsiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3JlcG9ydF9yZXRhaW5lZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6IjY1YTZiZWQ5MjNlNDVhNmFhMGM2MjQzMDQ1ZTAzNjRjOTBhY2UwNjQ4ZmVjYjViMGJiMmViZDk0MDI3OTNjNzciLCJjaGlsZF9zdGFydGVkIjp0cnVlLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlX2FydGlmYWN0X3NoYTI1NiI6ImNlYzM1N2FjZGI3NGY0ZTAyZGQxYzMyMDlhOGVjODgwOTE1Yzk4MzAxMzZlMDcyNjBmM2MxZjYzMWY4ZDUyZTgiLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV2aWRlbmNlX3NoYTI1NiI6IjM4MTMxOWMxZDc2MmEyM2MyMTE2YTJlZTA3NDE3NmMwNmY5Yzg0MjBiOTUxMzhlYWRiOWI2N2RhNmEzYjQ0NDciLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpZGVtcG90ZW5jeV9rZXlfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImxvc3NfY29kZSI6InJlcG9ydF9taXNzaW5nIiwibWFuYWdlZF9kaXNwYXRjaF9iaW5kaW5nX3NoYTI1NiI6InNoYTI1NjpjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwibm90X3JlY2VpdmVkX2NsYWltZWQiOmZhbHNlLCJvYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicGVuZGluZ19hY3Rpb25fcmVxdWVzdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwicGVuZGluZ19wZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInJlcG9ydF9wcm92ZW5hbmNlIjoib3V0ZXJfcnVubmVyX3JlY292ZXJ5IiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bl9zdG9yZV9pZGVudGl0eV9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQubWFuYWdlZC1yZXN1bHQtbG9zcy92MSJ9LCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MSwicGVybWl0X2NoYWluIjp7ImVudHJpZXMiOltdLCJwZW5kaW5nIjp7ImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3QiOm51bGwsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3Rfc2hhMjU2IjpudWxsLCJkZWxpdmVyeV9zdGF0ZSI6IlVOUkVTT0xWRUQiLCJwZXJtaXRfYXJ0aWZhY3QiOnsicGF5bG9hZCI6eyJhY3Rpb25fcmVxdWVzdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2FydGlmYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJhdXRob3JpdHlfc2VxdWVuY2UiOjAsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJldmlkZW5jZV9pZGVudGl0eV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsInBlcm1pdF9pZCI6InBlcm1pdDoxIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfY2hlY2tlZF9hdCI6IjIwMjYtMDgtMThUMTE6NTk6MzBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfZXhwaXJlc19hdCI6IjIwMjYtMDgtMjBUMTE6MDA6MDBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6ImM4N2Y1NzUxYWE5YjI3OWNjZTIzYWExNzhhZmY5NTZlMTlmNjkwZmNkMmY5ZjcwZTBkOTk5NzgyYTY5MGQ2YjgiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6IjBCeUhncUR4UVpybmdMSUYtNnRaT3E5bFhhbWNBUUMwZG9VVG5lS05QZVMtU3lvT2JReU9rQXVQRks1UFljTnpPYlp4Z3NnQUxkV2FJX3NhVUNwbEJnIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJwZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZW5kaW5nLXBlcm1pdC92MiJ9LCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTliMzk5NzFlMzA1MTA5YzU0Mjk5NDVhNWZhYjQ2NjBiMDlmNDFlN2FjM2ZmZDkyZTM1MDZhZjAwMzM3ZGE5NSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWNoYWluL3YyIn0sInBlcm1pdF9jb3VudCI6MSwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVjZWlwdCI6eyJhdXRob3JpemF0aW9uX2NvbmZpcm1lZCI6MSwiYXV0aG9yaXphdGlvbl9yZXF1aXJlZCI6MSwiYnVuZGxlX2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJkdXJhdGlvbl9tcyI6MCwiZWZmZWN0X3RpZXJfcmVhY2hlZCI6Im5vbmUiLCJlZmZlY3RzX2NvbmZpcm1lZCI6MCwiZWZmZWN0c19yZXF1aXJlZCI6MCwiZXN0X2Nvc3RfbWljcm91c2QiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiJdLCJleHRlcm5hbF9uZXR3b3JrX2NhbGxzIjoibm9uZSIsImdlbmVyYXRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiaGVhbHMiOjAsImlkZW50aXR5X2FwcGxpY2FibGUiOjAsImlkZW50aXR5X2FybWVkIjowLCJpZGVudGl0eV9jb25maXJtZWQiOjAsImlkZW50aXR5X3JlcXVpcmVkIjowLCJtYW5hZ2VkX3Jlc3VsdF9sb3NzX2V2aWRlbmNlX3NoYTI1NiI6IjM4MTMxOWMxZDc2MmEyM2MyMTE2YTJlZTA3NDE3NmMwNmY5Yzg0MjBiOTUxMzhlYWRiOWI2N2RhNmEzYjQ0NDciLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsIm92ZXJfaGFsdF9jb3VudCI6MCwicG9zdGNvbmRpdGlvbnNfY29uZmlybWVkIjowLCJwb3N0Y29uZGl0aW9uc19yZXF1aXJlZCI6MCwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInByb3ZlbmFuY2UiOiJwcm9kdWN0aW9uIiwicmVjZWlwdF9idWlsZGVyX3ZlcnNpb24iOiIxLjIuMyIsInJ1bmdfaGlzdG9ncmFtIjp7fSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1ydW4tcmVjZWlwdC92MSIsInNvdXJjZV9yZWNlaXB0X2RpZ2VzdCI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzb3VyY2VfcmVjZWlwdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3NjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnJ1bi1yZXBvcnQvdjEiLCJzdGVwc19vayI6MCwic3RlcHNfdG90YWwiOjEsInN1YnN0cmF0ZSI6IndlYiIsInRyYW5zYWN0aW9uX291dGNvbWUiOiJSRUNPTkNJTElBVElPTl9SRVFVSVJFRCJ9LCJydW5fcmVjZWlwdF9zaGEyNTYiOiJhN2RhZmIxNWFiODE4MzhmNGIzNGRhZDA4MTI2OTJhN2E1ZjE5ZTAwNzkzNWUwZDc2NTIwNDFiOGU3MDUwYWUyIiwicnVuX3JlcG9ydF9vYmplY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bl9yZXBvcnRfb2JqZWN0X3ZlcnNpb24iOiJ2ZXJzaW9uOm1hbmFnZWQtcmVzdWx0LWxvc3M6MSIsInJ1bl9yZXBvcnRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bnRpbWVfc3Vic3RyYXRlIjoid2ViIiwicnVudGltZV92YWxpZGF0aW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA1Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi10ZXJtaW5hbC12ZXJpZmljYXRpb24vdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9zZXF1ZW5jZSI6MSwidmVyaWZpZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJ3b3JrZmxvd19pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMyIsIndvcmtmbG93X3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQifSwic2lnbmF0dXJlIjoiVzRUMmpQelZ3azFoM2NuR1JoVkZ5aTgwalZyanZkandRR04tTVpyeFQ2R2pVejljbDV4UXloajd2LURabkxFTkFzcGlnLTBGeEV1WVJHeU9tWkdDQVEiLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImV2aWRlbmNlLXJ1bm5lci1lZDI1NTE5LTU2NDc1YWE3NTQ2MzQ3NGMiLCJwdWJsaWNfa2V5IjoiQTZFSHYvUE9FTDRkY04wWTUwdkFtV2ZrMWpDYnBRMWZIZHlHWkJKVk1iZz0ifX0=", + "managed_result_loss": { + "actuation_replay_authorized": false, + "authenticated_runner_id_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "authenticated_session_id_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "blind_retry_authorized": false, + "child_report_retained": false, + "child_start_evidence_sha256": "65a6bed923e45a6aa0c6243045e0364c90ace0648fecb5b0bb2ebd9402793c77", + "child_started": true, + "child_started_at": "2026-08-18T12:00:00Z", + "delivery_result_loss_closure_artifact_sha256": "cec357acdb74f4e02dd1c3209a8ec880915c9830136e07260f3c1f631f8d52e8", + "delivery_state": "CLOSED_UNRESOLVED_RESULT_LOSS", + "dispatch_id": "00000000-0000-4000-8000-000000000009", + "dispatch_session_id": "00000000-0000-4000-8000-000000000010", + "effect_absence_claimed": false, + "evidence_sha256": "381319c1d762a23c2116a2ee074176c06f9c8420b95138eadb9b67da6a3b4447", + "execution_authority_id": "00000000-0000-4000-8000-000000000008", + "execution_authority_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "execution_authority_signer_sha256": "24f6ed6acbfe1009c030d7ca567c33ca4830911498236b5561a6c82abec5de28", + "flow_run_id_sha256": "11e594f481958c10e3015d0bf0447a22f068a8a647f475df15ce2c7ab4b8f3f1", + "idempotency_key_sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "loss_code": "report_missing", + "managed_dispatch_binding_sha256": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "not_received_claimed": false, + "observed_at": "2026-08-18T12:00:02Z", + "pending_action_request_sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "pending_permit_artifact_sha256": "972a82af456da542bac784601889668891d8b7ccd242904816b3292e157e5289", + "report_provenance": "outer_runner_recovery", + "run_id": "00000000-0000-4000-8000-000000000001", + "run_request_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "run_store_identity_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "schema_version": "openadapt.managed-result-loss/v1" + }, + "name": "reconciliation-required-managed-result-loss", + "payload_canonical_sha256": "1480dcd4830987d01d27a66647e8b38af02a605d93ca604c5cbc50c10a7a1852", + "signature": "W4T2jPzVwk1h3cnGRhVFyi80jVrjvdjwQGN-MZrxT6GjUz9cl5xQyhj7v-DZnLENAspig-0FxEuYRGyOmZGCAQ", + "terminal_verification_artifact_sha256": "fa3eb5109494d421cefff1557211b19614fbf4be654c188832c8130c3308899e" + }, + { + "callback": { + "artifact_bytes_source": "envelope_canonical_base64", + "outcome": "RECONCILIATION_REQUIRED", + "report_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "run_id": "00000000-0000-4000-8000-000000000001", + "schema_version": "openadapt.hosted-runner-terminal/v1", + "started": true, + "uncertain_delivery": false + }, + "effect_state": null, + "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjEsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlIjp7InBheWxvYWQiOnsiYWNrbm93bGVkZ2VkX3Blcm1pdF9jb3VudCI6MSwiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6IjY1YTZiZWQ5MjNlNDVhNmFhMGM2MjQzMDQ1ZTAzNjRjOTBhY2UwNjQ4ZmVjYjViMGJiMmViZDk0MDI3OTNjNzciLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJjbG9zZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsImNsb3N1cmVfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTEiLCJjbG9zdXJlX3JlcXVlc3Rfc2hhMjU2IjoiYmFmNDE3YWU0OGVlYzQwNjU4NGVmNTI4NmFhYTFiMWFjMmU2ZTAzZDhhMjlhNGZiNGIwYzUzMjcwNGJlNTljNiIsImNsb3N1cmVfc2VxdWVuY2UiOjEsImRlbGl2ZXJ5X2Fja25vd2xlZGdtZW50X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9pbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbXBvdGVuY3lfa2V5X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJuZXdfcGVybWl0X2F1dGhvcml6ZWQiOmZhbHNlLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsInBlbmRpbmdfYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjpudWxsLCJwZW5kaW5nX3Blcm1pdF9hcnRpZmFjdF9zaGEyNTYiOm51bGwsInBlbmRpbmdfcGVybWl0X2NvdW50IjowLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInBlcm1pdF9jb3VudCI6MSwicmVjZWlwdF9hYnNlbmNlX29ic2VydmVkX2F0IjpudWxsLCJyZXN1bHRfbG9zc19vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bl9zdG9yZV9pZGVudGl0eV9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZXN1bHQtbG9zcy1jbG9zdXJlLXBheWxvYWQvdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9jYWxsYmFja19yZXF1aXJlZCI6dHJ1ZX0sInBheWxvYWRfc2hhMjU2IjoiMTE5YjBkODA2Njk0ZWQyMTYwNGQwOTE3MTMyMmQ1MjFiYzBhYTEyYjFlYTc5ZmIzNDkyNTA0YWMzZWZjNjRlYSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6InZsUmhUdHFSdm9kYnNObWZ5MmFSU3pndU0tYmZkZDRUWkFyeHBvbGRHd0NSeWtDZWRVLUZhanhkdHlyTDd5TmNpQnZpdWNvTnBpUFUyQXBNdG9abEJnIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6ImYwY2ExOWE2YjNlYWJlNTk0ZGJjOTA2ODljMmY3NGNiZTVlODJkNGQ5NDdkYjI5YTVkMDdiMTY0MWU4OTYxY2YiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6IjdlMTJkY2E1YzQ0Y2Y5OThhNDY5MzYyYjg2MmU4MTZhN2QyYzg1NDE2M2Y2M2U4ZGU2MWZiNzllOTViYTgwMDciLCJyZWNvcmRzIjpbXSwicmVxdWlyZWQiOjAsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZWZmZWN0LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJpZGVudGl0eSI6eyJjb25maXJtZWQiOjAsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiJkM2E3ZWFjMzQyN2Q3NmY2YmMzNjAzMDczODYwOTExOWYwN2UwZDkxYTZkMzU4YWJjZTViNmYwZDZkZjc4NjQ1IiwicmVxdWlyZWQiOjAsInJlc3VsdHMiOltdLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWlkZW50aXR5LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJwb2xpY3kiOnsiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX3BvbGljeV9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZ292ZXJuZWRfcnVudGltZV9pbnB1dHNfZGlnZXN0IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiI0Zjk4OGQwMTViY2Q3Y2Q3ZTA1YmEwNjYzZDg5ZjBhOTY4ZjBkOGFjZTk5YjA5MGViYWJlZTUyYTFjNTUwNDIxIiwibWluaW11bV9lZmZlY3RfdGllciI6MSwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb2xpY3ktZXZpZGVuY2UvdjEifSwicG9zdGNvbmRpdGlvbiI6eyJjb25maXJtZWQiOjAsIm1hbmlmZXN0X3NoYTI1NiI6IjUyZjA5ZDVkNjM5YjFjMzgzZjM3Nzk2M2U0ODdjYjg5ZDg5NDE3ODU3ZTQ0ZGE3ZTNmMjNlNTViZTc1MWU3ZjQiLCJyZWNvcmRzIjpbXSwicmVxdWlyZWQiOjAsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9zdGNvbmRpdGlvbi1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifX0sImV2aWRlbmNlX3J1bm5lcl9zaWduZXJfc2hhMjU2IjoiNTY0NzVhYTc1NDYzNDc0YzAyODVkZjVkYmYyYmNhYjczZGE2NTEzNTg4MzllOWI3NzQ4MWIyZWFiMTA3NzA4YyIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZXhlY3V0aW9uX291dGNvbWUiOnsiY29tcGVuc2F0aW9uX2FjdGlvbnMiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiJdLCJleGVjdXRpb25fY29tcGxldGVkIjpmYWxzZSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJtYW5hZ2VkX3Jlc3VsdF9sb3NzX2V2aWRlbmNlX3NoYTI1NiI6IjcxNDUyYjExODgzYWJjZDYzZDhjNzYxMzNkNDI2MDEyMjhmYWExYWZlY2Q4Mjc5YjVlY2NlODY2MThkNGViMjMiLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsInBhc3NlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjowLCJpZGVudGl0eSI6MCwicG9zdGNvbmRpdGlvbiI6MH0sInBvc3Rjb25kaXRpb25fZXZpZGVuY2UiOltdLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjpmYWxzZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicXVhbGlmaWNhdGlvbl9ldmlkZW5jZV9vbmx5IjpmYWxzZSwicmVxdWlyZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjAsInBvc3Rjb25kaXRpb24iOjB9LCJ2ZXJzaW9uIjoib3BlbmFkYXB0LmV4ZWN1dGlvbi1vdXRjb21lL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJleGVjdXRpb25fb3V0Y29tZV9zaGEyNTYiOiI0ODlhYmU5MzQ0OGNjYzc5NWZhZWY0NGJjZTcwMTE1MzQwODM2ZDVjNDc1ZDIwZGIzZmNmOWY2NTY2NTJmYzRkIiwiZXhlY3V0aW9uX3B1cnBvc2UiOiJwcm9kdWN0aW9uIiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDNaIiwibWFuYWdlZF9yZXN1bHRfbG9zcyI6eyJhY3R1YXRpb25fcmVwbGF5X2F1dGhvcml6ZWQiOmZhbHNlLCJhdXRoZW50aWNhdGVkX3J1bm5lcl9pZF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYXV0aGVudGljYXRlZF9zZXNzaW9uX2lkX3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJibGluZF9yZXRyeV9hdXRob3JpemVkIjpmYWxzZSwiY2hpbGRfcmVwb3J0X3JldGFpbmVkIjpmYWxzZSwiY2hpbGRfc3RhcnRfZXZpZGVuY2Vfc2hhMjU2IjoiNjVhNmJlZDkyM2U0NWE2YWEwYzYyNDMwNDVlMDM2NGM5MGFjZTA2NDhmZWNiNWIwYmIyZWJkOTQwMjc5M2M3NyIsImNoaWxkX3N0YXJ0ZWQiOnRydWUsImNoaWxkX3N0YXJ0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImRlbGl2ZXJ5X3Jlc3VsdF9sb3NzX2Nsb3N1cmVfYXJ0aWZhY3Rfc2hhMjU2IjoiOTBkZjhiZDFmMjg5MWIyMDM2NjNhNjNmNDI1MTUwNzQ0NWRiZTgzODM2MGEyMTllNDNhZjlhMWJjZTBhODBhMSIsImRlbGl2ZXJ5X3N0YXRlIjoiQ0xPU0VEX1VOUkVTT0xWRURfUkVTVUxUX0xPU1MiLCJkaXNwYXRjaF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOSIsImRpc3BhdGNoX3Nlc3Npb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTAiLCJlZmZlY3RfYWJzZW5jZV9jbGFpbWVkIjpmYWxzZSwiZXZpZGVuY2Vfc2hhMjU2IjoiNzE0NTJiMTE4ODNhYmNkNjNkOGM3NjEzM2Q0MjYwMTIyOGZhYTFhZmVjZDgyNzliNWVjY2U4NjYxOGQ0ZWIyMyIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW1wb3RlbmN5X2tleV9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibG9zc19jb2RlIjoicmVwb3J0X21pc3NpbmciLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsIm9ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJwZW5kaW5nX2FjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6bnVsbCwicGVuZGluZ19wZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjpudWxsLCJyZXBvcnRfcHJvdmVuYW5jZSI6Im91dGVyX3J1bm5lcl9yZWNvdmVyeSIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW5fc3RvcmVfaWRlbnRpdHlfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0Lm1hbmFnZWQtcmVzdWx0LWxvc3MvdjEifSwicGVuZGluZ19wZXJtaXRfY291bnQiOjAsInBlcm1pdF9jaGFpbiI6eyJlbnRyaWVzIjpbeyJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiZGVsaXZlcmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMVoiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4Iiwib25lX3VzZV9jbGFpbV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsInBlcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5IiwicGVybWl0X2lkIjoicGVybWl0OjEiLCJydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6IjhhOGY2ZDg4NzUwY2JiNmIzYmQ2NjIwOTNhZDI5ZTJiYzIwYzZhYjY5OTFkNzBlNTg4NjkxYzhiM2FiNjYyYjkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJBT0txQ2dTMXMwWGEtaXlFOTZWLXlsUHpRY3YydXZPc3V5Z0h6dnUyUnlWN0ZMc0hGX0xWZnF5eDR1MnB1bWxFbDBZazVZdTcwSlZVa3NacGFVcEREdyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwiZGVsaXZlcnlfcmVjZWlwdF9hcnRpZmFjdF9zaGEyNTYiOiJjMDBhMWJhNzAxNWYzY2JlMDVhNzZhZWFiZmY2MTlkNzkzZTczNzVhMzEwZTE5NWZjYmJkMmQyOTQ4ZjJiMjE0IiwicGVybWl0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJwZXJtaXRfaWQiOiJwZXJtaXQ6MSIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2NoZWNrZWRfYXQiOiIyMDI2LTA4LTE4VDExOjU5OjMwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2V4cGlyZXNfYXQiOiIyMDI2LTA4LTIwVDExOjAwOjAwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiJjODdmNTc1MWFhOWIyNzljY2UyM2FhMTc4YWZmOTU2ZTE5ZjY5MGZjZDJmOWY3MGUwZDk5OTc4MmE2OTBkNmI4Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiIwQnlIZ3FEeFFacm5nTElGLTZ0Wk9xOWxYYW1jQVFDMGRvVVRuZUtOUGVTLVN5b09iUXlPa0F1UEZLNVBZY056T2JaeGdzZ0FMZFdhSV9zYVVDcGxCZyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwicGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi1lbnRyeS92MiJ9XSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjUwNDhmZTk0NTg4MmQyODAyNjAwOGQ3ZTkxMzdhZDAxNmZkZDAxYWVhZjU4ODQ4MWZiNWJkMGYyNDJhNTAwNDMiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi92MiJ9LCJwZXJtaXRfY291bnQiOjEsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlY2VpcHQiOnsiYXV0aG9yaXphdGlvbl9jb25maXJtZWQiOjEsImF1dGhvcml6YXRpb25fcmVxdWlyZWQiOjEsImJ1bmRsZV9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZHVyYXRpb25fbXMiOjAsImVmZmVjdF90aWVyX3JlYWNoZWQiOiJub25lIiwiZWZmZWN0c19jb25maXJtZWQiOjAsImVmZmVjdHNfcmVxdWlyZWQiOjAsImVzdF9jb3N0X21pY3JvdXNkIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iXSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJnZW5lcmF0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImhlYWxzIjowLCJpZGVudGl0eV9hcHBsaWNhYmxlIjowLCJpZGVudGl0eV9hcm1lZCI6MCwiaWRlbnRpdHlfY29uZmlybWVkIjowLCJpZGVudGl0eV9yZXF1aXJlZCI6MCwibWFuYWdlZF9yZXN1bHRfbG9zc19ldmlkZW5jZV9zaGEyNTYiOiI3MTQ1MmIxMTg4M2FiY2Q2M2Q4Yzc2MTMzZDQyNjAxMjI4ZmFhMWFmZWNkODI3OWI1ZWNjZTg2NjE4ZDRlYjIzIiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJvdmVyX2hhbHRfY291bnQiOjAsInBvc3Rjb25kaXRpb25zX2NvbmZpcm1lZCI6MCwicG9zdGNvbmRpdGlvbnNfcmVxdWlyZWQiOjAsInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJwcm92ZW5hbmNlIjoicHJvZHVjdGlvbiIsInJlY2VpcHRfYnVpbGRlcl92ZXJzaW9uIjoiMS4yLjMiLCJydW5nX2hpc3RvZ3JhbSI6e30sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcnVuLXJlY2VpcHQvdjEiLCJzb3VyY2VfcmVjZWlwdF9kaWdlc3QiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3JlY2VpcHRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9zY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5ydW4tcmVwb3J0L3YxIiwic3RlcHNfb2siOjAsInN0ZXBzX3RvdGFsIjoxLCJzdWJzdHJhdGUiOiJ3ZWIiLCJ0cmFuc2FjdGlvbl9vdXRjb21lIjoiUkVDT05DSUxJQVRJT05fUkVRVUlSRUQifSwicnVuX3JlY2VpcHRfc2hhMjU2IjoiNDhiZDFkNmRjOTVkYmNhOTlhMTM4ZDE2ZjIzMmNkNjkxMDI1ZThhNGUxM2E0OGFlYTA4ZTI4NmRkMjA2NzlhNSIsInJ1bl9yZXBvcnRfb2JqZWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW5fcmVwb3J0X29iamVjdF92ZXJzaW9uIjoidmVyc2lvbjptYW5hZ2VkLXJlc3VsdC1sb3NzOjEiLCJydW5fcmVwb3J0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW50aW1lX3N1YnN0cmF0ZSI6IndlYiIsInJ1bnRpbWVfdmFsaWRhdGlvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfc2VxdWVuY2UiOjEsInZlcmlmaWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwid29ya2Zsb3dfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJ3b3JrZmxvd192ZXJzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA0In0sInNpZ25hdHVyZSI6ImM2OFVFYUdXejVfb2tRMDN2cGZjWGJOS1poWEE1Y2FfV3dpaTBPeGdhVGI3RzJXc1FFZGVjZGI0Z0tqVDIzck5FRGtBRTFaQzRBb1ZpTUdXcFRlaERRIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJldmlkZW5jZS1ydW5uZXItZWQyNTUxOS01NjQ3NWFhNzU0NjM0NzRjIiwicHVibGljX2tleSI6IkE2RUh2L1BPRUw0ZGNOMFk1MHZBbVdmazFqQ2JwUTFmSGR5R1pCSlZNYmc9In19", + "managed_result_loss": { + "actuation_replay_authorized": false, + "authenticated_runner_id_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "authenticated_session_id_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "blind_retry_authorized": false, + "child_report_retained": false, + "child_start_evidence_sha256": "65a6bed923e45a6aa0c6243045e0364c90ace0648fecb5b0bb2ebd9402793c77", + "child_started": true, + "child_started_at": "2026-08-18T12:00:00Z", + "delivery_result_loss_closure_artifact_sha256": "90df8bd1f2891b203663a63f4251507445dbe838360a219e43af9a1bce0a80a1", + "delivery_state": "CLOSED_UNRESOLVED_RESULT_LOSS", + "dispatch_id": "00000000-0000-4000-8000-000000000009", + "dispatch_session_id": "00000000-0000-4000-8000-000000000010", + "effect_absence_claimed": false, + "evidence_sha256": "71452b11883abcd63d8c76133d42601228faa1afecd8279b5ecce86618d4eb23", + "execution_authority_id": "00000000-0000-4000-8000-000000000008", + "execution_authority_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "execution_authority_signer_sha256": "24f6ed6acbfe1009c030d7ca567c33ca4830911498236b5561a6c82abec5de28", + "flow_run_id_sha256": "11e594f481958c10e3015d0bf0447a22f068a8a647f475df15ce2c7ab4b8f3f1", + "idempotency_key_sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "loss_code": "report_missing", + "managed_dispatch_binding_sha256": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "not_received_claimed": false, + "observed_at": "2026-08-18T12:00:02Z", + "pending_action_request_sha256": null, + "pending_permit_artifact_sha256": null, + "report_provenance": "outer_runner_recovery", + "run_id": "00000000-0000-4000-8000-000000000001", + "run_request_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "run_store_identity_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "schema_version": "openadapt.managed-result-loss/v1" + }, + "name": "reconciliation-required-managed-result-loss-acknowledged", + "payload_canonical_sha256": "f636736f6796ba6246fdd2d6c0668642928ad5990342e801b882b2c22013eae3", + "signature": "c68UEaGWz5_okQ03vpfcXbNKZhXA5ca_Wwii0OxgaTb7G2WsQEdecdb4gKjT23rNEDkAE1ZC4AoViMGWpTehDQ", + "terminal_verification_artifact_sha256": "b78ff44e81d4be51fbb28132c871aab660616a8ec5eba0dcde24e6298c0cd87f" } ] } diff --git a/tests/test_hosted_runner_adapter.py b/tests/test_hosted_runner_adapter.py index 11de044f..1dae494f 100644 --- a/tests/test_hosted_runner_adapter.py +++ b/tests/test_hosted_runner_adapter.py @@ -4,6 +4,7 @@ import json import os from base64 import b64decode, b64encode, urlsafe_b64encode +from concurrent.futures import ThreadPoolExecutor from dataclasses import replace from datetime import datetime, timezone from pathlib import Path @@ -59,17 +60,24 @@ ProductionDeliveryPermitChain, ProductionDeliveryPermitPayload, ProductionDeliveryReceiptPayload, + ProductionDeliveryResultLossClosurePayload, + ProductionPendingDeliveryPermit, delivery_authority_signer_sha256, evidence_runner_signer_sha256, sign_production_delivery_permit, sign_production_delivery_receipt, + sign_production_delivery_result_loss_closure, sign_production_terminal_verification, ) from openadapt_flow.transaction import TransactionOutcome from tests.test_run_receipt import _report as _production_report from tests.test_runner_client_lib import dispatch_payload from tests.test_terminal_verification_v2 import ( + IDS, + _acknowledged_reconciliation_payload, _halted_payload, + _managed_result_loss_acknowledged_payload, + _managed_result_loss_payload, _payload, _private_key, _reconciliation_payload, @@ -500,6 +508,78 @@ def _terminal_delivery_chain( ) +def _result_loss_closure_result( + dispatch: HostedDispatch, + marker: hosted.ManagedChildStartEvidence, + request: hosted.ProductionDeliveryResultLossClosureRequest, + chain: ProductionDeliveryPermitChain, +) -> hosted.ProductionDeliveryResultLossClosureResult: + all_permits = (*chain.entries, *((chain.pending,) if chain.pending else ())) + assert all_permits + first = all_permits[0] + final = all_permits[-1] + pending = chain.pending + acknowledged = chain.entries[-1] if chain.entries else None + payload = ProductionDeliveryResultLossClosurePayload( + closure_id="00000000-0000-4000-8000-000000000011", + closure_request_sha256=request.request_sha256(), + closed_at=request.result_loss_observed_at, + result_loss_observed_at=request.result_loss_observed_at, + receipt_absence_observed_at=( + pending.receipt_absence_observed_at if pending is not None else None + ), + tenant_id=dispatch.tenant_id, + run_id=dispatch.run_id, + flow_run_id_sha256=hashlib.sha256(dispatch.run_id.encode("utf-8")).hexdigest(), + dispatch_id=dispatch.dispatch_id, + dispatch_session_id=dispatch.dispatch_session_id, + managed_dispatch_binding_sha256=dispatch.payload.dispatch_binding_sha256, + idempotency_key_sha256=hosted.managed_result_loss_idempotency_sha256( + dispatch.idempotency_key + ), + authenticated_runner_id_sha256=hashlib.sha256( + dispatch.runner_id.encode("utf-8") + ).hexdigest(), + authenticated_session_id_sha256=hashlib.sha256( + dispatch.runner_session_id.encode("utf-8") + ).hexdigest(), + execution_authority_id=dispatch.execution_authority_id, + execution_authority_sha256=dispatch.execution_authority_sha256, + execution_authority_signer_sha256=(dispatch.execution_authority_signer_sha256), + child_started_at=marker.started_at, + child_start_evidence_sha256=marker.marker_sha256, + run_store_identity_sha256=marker.run_store_identity_sha256, + permit_chain_sha256=chain.permit_chain_sha256, + permit_count=len(all_permits), + acknowledged_permit_count=len(chain.entries), + pending_permit_count=1 if pending else 0, + pending_permit_artifact_sha256=( + pending.permit_artifact_sha256 if pending else None + ), + run_request_sha256=first.run_request_sha256, + pending_action_request_sha256=( + pending.action_request_sha256 if pending else None + ), + final_input_edge_sequence=final.input_edge_sequence, + final_authority_sequence=final.authority_sequence, + final_runtime_delivery_sequence=( + acknowledged.runtime_delivery_sequence if acknowledged else 0 + ), + ) + artifact = sign_production_delivery_result_loss_closure( + payload, + Ed25519PrivateKey.from_private_bytes(bytes(range(32, 64))), + ) + artifact_raw = hosted.canonical_json(artifact) + chain_raw = hosted.canonical_json(chain) + return hosted.ProductionDeliveryResultLossClosureResult( + closure_artifact_bytes_base64=b64encode(artifact_raw).decode("ascii"), + closure_artifact_sha256=artifact.artifact_sha256(), + permit_chain_bytes_base64=b64encode(chain_raw).decode("ascii"), + permit_chain_sha256=chain.permit_chain_sha256, + ) + + def test_outer_adapter_builds_stores_rereads_and_verifies_terminal_v2( monkeypatch, tmp_path, sealed ) -> None: @@ -687,8 +767,11 @@ def fail_proof_reread(path, *, maximum_bytes, label): verified_params=verified_params, dispatch_binding_sha256=binding, ) - assert not (storage_failure_dir / "production-terminal-report.json").exists() - assert not (storage_failure_dir / "production-terminal-verification.json").exists() + # The one-use terminal CAS remains authoritative after a component reread + # fault. Removing it would let a late result replace a sealed outcome. + assert (storage_failure_dir / "production-terminal-state.json").is_file() + assert (storage_failure_dir / "production-terminal-report.json").is_file() + assert (storage_failure_dir / "production-terminal-verification.json").is_file() monkeypatch.setattr(adapter, "_read_private_bytes", original_read) proof, report_sha256 = adapter._produce_terminal_verification( @@ -714,6 +797,82 @@ def fail_proof_reread(path, *, maximum_bytes, label): assert "2026-07-01" in report_path.read_text(encoding="utf-8") assert "2026-07-01" not in proof_path.read_text(encoding="utf-8") + replayed_proof, replayed_report_sha256 = adapter._produce_terminal_verification( + dispatch=dispatch, + report=report, + run_dir=run_dir, + qualification=qualification, + private_key=private_key, + verified_params=verified_params, + dispatch_binding_sha256=binding, + ) + assert hosted.canonical_json(replayed_proof) == hosted.canonical_json(proof) + assert replayed_report_sha256 == report_sha256 + + late_report = report.model_copy(update={"total_ms": report.total_ms + 1}) + with pytest.raises(ValueError, match="different terminal outcome"): + adapter._produce_terminal_verification( + dispatch=dispatch, + report=late_report, + run_dir=run_dir, + qualification=qualification, + private_key=private_key, + verified_params=verified_params, + dispatch_binding_sha256=binding, + ) + assert proof_path.read_bytes() == hosted.canonical_json(proof) + + race_dir = tmp_path / "terminal-race" + race_dir.mkdir(mode=0o700) + + def terminalize_once(_index): + return adapter._produce_terminal_verification( + dispatch=dispatch, + report=report, + run_dir=race_dir, + qualification=qualification, + private_key=private_key, + verified_params=verified_params, + dispatch_binding_sha256=binding, + ) + + with ThreadPoolExecutor(max_workers=4) as pool: + raced = list(pool.map(terminalize_once, range(4))) + assert {hosted.canonical_json(item[0]) for item in raced} == { + hosted.canonical_json(proof) + } + assert {item[1] for item in raced} == {report_sha256} + + advancing_race_dir = tmp_path / "advancing-terminal-race" + advancing_race_dir.mkdir(mode=0o700) + + class AdvancingDatetime(datetime): + calls = 0 + + @classmethod + def now(cls, tz=None): + cls.calls += 1 + value = fixed_now.replace(second=2 + cls.calls) + return value if tz is not None else value.replace(tzinfo=None) + + monkeypatch.setattr(hosted, "datetime", AdvancingDatetime) + + def advancing_terminalize_once(_index): + return adapter._produce_terminal_verification( + dispatch=dispatch, + report=report, + run_dir=advancing_race_dir, + qualification=qualification, + private_key=private_key, + verified_params=verified_params, + dispatch_binding_sha256=binding, + ) + + with ThreadPoolExecutor(max_workers=4) as pool: + advancing_raced = list(pool.map(advancing_terminalize_once, range(4))) + assert len({hosted.canonical_json(item[0]) for item in advancing_raced}) == 1 + assert {item[1] for item in advancing_raced} == {report_sha256} + def test_terminal_admission_is_revalidated_after_child_execution( monkeypatch, tmp_path, config, sealed @@ -847,6 +1006,453 @@ def runner(argv, _run_dir, child_env): } +@pytest.mark.parametrize( + ("execution", "expected_loss_code"), + [ + (RuntimeError("child exit result lost"), "runner_exception"), + (OSError("child transport result lost"), "runner_exception"), + ( + ManagedExecution(returncode=1, report_bytes=None), + "report_missing", + ), + ], + ids=("runtime-exception", "transport-exception", "missing-report"), +) +def test_expected_result_loss_returns_one_signed_reconciliation_without_retry( + monkeypatch, + tmp_path, + config, + sealed, + execution, + expected_loss_code, +) -> None: + workflow, _ = sealed + calls = 0 + + def runner(_argv, _run_dir, _child_env): + nonlocal calls + calls += 1 + if isinstance(execution, Exception): + raise execution + return execution + + adapter, dispatch = _prepared_adapter( + monkeypatch, tmp_path, config, workflow, runner + ) + payload = dispatch.payload.model_copy( + update={ + "run_id": IDS["run_id"], + "workflow_id": IDS["workflow_id"], + } + ) + payload = payload.model_copy( + update={ + "dispatch_binding_sha256": dispatch_binding_sha256( + IDS["run_id"], payload.authorization + ) + } + ) + dispatch = HostedDispatch.model_validate( + { + **dispatch.model_dump(mode="json"), + "run_id": IDS["run_id"], + "workflow_id": IDS["workflow_id"], + "payload": payload.model_dump(mode="json"), + } + ) + proof = sign_production_terminal_verification( + _managed_result_loss_payload(), _private_key() + ) + report = _production_report() + seen_loss_codes: list[str] = [] + + def produce_loss(**kwargs): + seen_loss_codes.append(kwargs["loss_code"]) + return proof, proof.payload.run_report_sha256, report + + monkeypatch.setattr(adapter, "_produce_managed_result_loss", produce_loss) + authority = DeliveryAuthority( + dispatch.managed_delivery_authority_url, + dispatch.delivery_authority_token, + ) + + result = adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run", + authority=authority, + closure_authority=SimpleNamespace(), + ) + callback = adapter.callback_request(dispatch, result) + + assert calls == 1 + assert seen_loss_codes == [expected_loss_code] + assert result.outcome is TransactionOutcome.RECONCILIATION_REQUIRED + assert result.uncertain_delivery is True + assert result.terminal_verification == proof + assert ( + len( + [ + event + for event in callback.events + if event.get("schema_version") == "openadapt.hosted-runner-terminal/v1" + ] + ) + == 1 + ) + assert callback.events[-1]["outcome"] == "RECONCILIATION_REQUIRED" + assert callback.events[-1]["uncertain_delivery"] is True + with pytest.raises(RuntimeError, match="signed terminal reconciliation"): + adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run-again", + authority=authority, + ) + assert calls == 1 + + +def test_managed_result_loss_report_is_outer_recovery_only(tmp_path, sealed) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite") + run_dir = tmp_path / "run" + run_dir.mkdir(mode=0o700) + marker = hosted.ManagedChildStartEvidence.create( + started_at="2026-08-26T12:00:00Z", + dispatch_id=dispatch.dispatch_id, + dispatch_session_id=dispatch.dispatch_session_id, + run_id=dispatch.run_id, + managed_dispatch_binding_sha256=dispatch.payload.dispatch_binding_sha256, + authenticated_runner_id_sha256=hashlib.sha256( + dispatch.runner_id.encode("utf-8") + ).hexdigest(), + authenticated_session_id_sha256=hashlib.sha256( + dispatch.runner_session_id.encode("utf-8") + ).hexdigest(), + execution_authority_id=dispatch.execution_authority_id, + execution_authority_sha256=dispatch.execution_authority_sha256, + execution_authority_signer_sha256=(dispatch.execution_authority_signer_sha256), + run_store_identity_sha256=adapter._run_store_identity_sha256(run_dir), + ) + loss = hosted.ManagedResultLossEvidence.create( + loss_code="report_missing", + child_started_at=marker.started_at, + child_start_evidence_sha256=marker.marker_sha256, + run_store_identity_sha256=marker.run_store_identity_sha256, + observed_at="2026-08-26T12:00:02Z", + run_id=dispatch.run_id, + flow_run_id_sha256=hashlib.sha256(dispatch.run_id.encode("utf-8")).hexdigest(), + dispatch_id=dispatch.dispatch_id, + dispatch_session_id=dispatch.dispatch_session_id, + managed_dispatch_binding_sha256=dispatch.payload.dispatch_binding_sha256, + idempotency_key_sha256=hosted.managed_result_loss_idempotency_sha256( + dispatch.idempotency_key + ), + authenticated_runner_id_sha256=marker.authenticated_runner_id_sha256, + authenticated_session_id_sha256=(marker.authenticated_session_id_sha256), + execution_authority_id=dispatch.execution_authority_id, + execution_authority_sha256=dispatch.execution_authority_sha256, + execution_authority_signer_sha256=(dispatch.execution_authority_signer_sha256), + delivery_result_loss_closure_artifact_sha256="d" * 64, + pending_permit_artifact_sha256="a" * 64, + run_request_sha256="b" * 64, + pending_action_request_sha256="c" * 64, + ) + manifest = SimpleNamespace( + governed_authorization=dispatch.payload.authorization.model_copy( + update={"execution_profile": "standard"} + ), + model_calls=0, + ) + + report = adapter._managed_result_loss_report( + dispatch=dispatch, + workflow=workflow, + manifest=manifest, + marker=marker, + evidence=loss, + verified_params=dict(dispatch.payload.params.values), + runtime_substrate="web", + ) + + assert report.managed_result_loss is not None + assert report.managed_result_loss.report_provenance == "outer_runner_recovery" + assert report.execution_outcome == "HALTED" + assert report.transaction_outcome == "RECONCILIATION_REQUIRED" + assert report.transaction_billable is False + assert report.effect_journal == [] + assert report.outcome_envelope is not None + assert report.outcome_envelope.required_contracts.model_dump() == { + "authorization": 1, + "identity": 0, + "postcondition": 0, + "effect": 0, + } + invalid = report.model_dump(mode="json") + invalid["success"] = True + with pytest.raises(ValueError, match="managed result loss report contract"): + hosted.RunReport.model_validate(invalid) + + +def test_concurrent_managed_result_loss_terminalizers_reuse_one_exact_record( + monkeypatch, tmp_path, sealed +) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite") + run_dir = tmp_path / "run" + run_dir.mkdir(mode=0o700) + binding = dispatch.payload.dispatch_binding_sha256 + marker = hosted.ManagedChildStartEvidence.create( + started_at="2026-08-26T11:59:59Z", + dispatch_id=dispatch.dispatch_id, + dispatch_session_id=dispatch.dispatch_session_id, + run_id=dispatch.run_id, + managed_dispatch_binding_sha256=binding, + authenticated_runner_id_sha256=hashlib.sha256( + dispatch.runner_id.encode("utf-8") + ).hexdigest(), + authenticated_session_id_sha256=hashlib.sha256( + dispatch.runner_session_id.encode("utf-8") + ).hexdigest(), + execution_authority_id=dispatch.execution_authority_id, + execution_authority_sha256=dispatch.execution_authority_sha256, + execution_authority_signer_sha256=(dispatch.execution_authority_signer_sha256), + run_store_identity_sha256=adapter._run_store_identity_sha256(run_dir), + ) + adapter._write_private_bytes_atomic_exclusive( + run_dir / "managed-child-started.json", hosted.canonical_json(marker) + ) + acknowledged = _terminal_delivery_chain( + dispatch, + admission_sha256="d" * 64, + evidence_identity_sha256="e" * 64, + environment_digest="a" * 64, + registry_sha256="e" * 64, + ) + assert acknowledged.entries + permit = acknowledged.entries[0].permit_artifact + + class Store: + def __init__(self, _run_dir): + pass + + def read_manifest(self): + return SimpleNamespace() + + class ClosureAuthority: + requests: list[hosted.ProductionDeliveryResultLossClosureRequest] = [] + + def close_result_loss(self, run_id, lease_token, request): + assert run_id == dispatch.run_id + assert lease_token == dispatch.lease_token + assert "oal_" not in request.canonical_bytes().decode("utf-8") + self.requests.append(request) + pending = ProductionPendingDeliveryPermit.build( + permit, + receipt_absence_observed_at=request.result_loss_observed_at, + ) + chain = ProductionDeliveryPermitChain.build((), pending=pending) + return _result_loss_closure_result(dispatch, marker, request, chain) + + closure_authority = ClosureAuthority() + + class RacingDatetime(datetime): + calls = 0 + + @classmethod + def now(cls, tz=None): + cls.calls += 1 + second = 1 + cls.calls + value = datetime(2026, 8, 26, 12, 0, second, tzinfo=timezone.utc) + return value if tz is not None else value.replace(tzinfo=None) + + monkeypatch.setattr(hosted, "CheckpointStore", Store) + monkeypatch.setattr(hosted, "datetime", RacingDatetime) + monkeypatch.setattr( + adapter, + "_managed_result_loss_report", + lambda **kwargs: kwargs["evidence"], + ) + monkeypatch.setattr( + adapter, + "_produce_terminal_verification", + lambda **kwargs: ( + kwargs["report"], + kwargs["report"].evidence_sha256, + ), + ) + qualification = SimpleNamespace( + expected=SimpleNamespace( + runtime_build_identity=SimpleNamespace(substrate="web") + ) + ) + + def terminalize(loss_code, closer=closure_authority): + return adapter._produce_managed_result_loss( + dispatch=dispatch, + workflow=workflow, + run_dir=run_dir, + qualification=qualification, + private_key=object(), + verified_params=dict(dispatch.payload.params.values), + dispatch_binding_sha256=binding, + closure_authority=closer, + loss_code=loss_code, + ) + + with ThreadPoolExecutor(max_workers=3) as pool: + results = list( + pool.map( + terminalize, + ("runner_exception", "report_missing", "recovered_after_restart"), + ) + ) + + retained = (run_dir / "managed-result-loss.json").read_bytes() + snapshot = hosted.ManagedResultLossSnapshot.model_validate_json(retained) + evidence_bytes = hosted.canonical_json(snapshot.evidence) + assert hosted.canonical_json(snapshot) == retained + assert {hosted.canonical_json(item[0]) for item in results} == {evidence_bytes} + assert {hosted.canonical_json(item[2]) for item in results} == {evidence_bytes} + assert {item[1] for item in results} == {snapshot.evidence.evidence_sha256} + assert closure_authority.requests + assert len({item.request_sha256() for item in closure_authority.requests}) == 1 + + class RefuseClosureReplay: + def close_result_loss(self, *_args, **_kwargs): + raise AssertionError("retained result loss requested a second closure") + + replayed = terminalize("recovered_after_restart", RefuseClosureReplay()) + assert hosted.canonical_json(replayed[0]) == evidence_bytes + assert hosted.canonical_json(replayed[2]) == evidence_bytes + if os.name != "nt": + assert (run_dir / "managed-result-loss.json").stat().st_mode & 0o777 == 0o600 + + +@pytest.mark.parametrize("pending", [True, False], ids=("pending", "ack-won")) +def test_result_loss_closure_response_binds_exact_request_and_chain( + tmp_path, sealed, pending +) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite") + run_dir = tmp_path / "run" + run_dir.mkdir(mode=0o700) + marker = hosted.ManagedChildStartEvidence.create( + started_at="2026-08-26T11:59:59Z", + dispatch_id=dispatch.dispatch_id, + dispatch_session_id=dispatch.dispatch_session_id, + run_id=dispatch.run_id, + managed_dispatch_binding_sha256=dispatch.payload.dispatch_binding_sha256, + authenticated_runner_id_sha256=hashlib.sha256( + dispatch.runner_id.encode("utf-8") + ).hexdigest(), + authenticated_session_id_sha256=hashlib.sha256( + dispatch.runner_session_id.encode("utf-8") + ).hexdigest(), + execution_authority_id=dispatch.execution_authority_id, + execution_authority_sha256=dispatch.execution_authority_sha256, + execution_authority_signer_sha256=(dispatch.execution_authority_signer_sha256), + run_store_identity_sha256=adapter._run_store_identity_sha256(run_dir), + ) + request = hosted.ProductionDeliveryResultLossClosureRequest( + child_start_evidence=marker, + result_loss_observed_at="2026-08-26T12:00:02Z", + ) + acknowledged = _terminal_delivery_chain( + dispatch, + admission_sha256="d" * 64, + evidence_identity_sha256="e" * 64, + environment_digest="a" * 64, + registry_sha256="e" * 64, + ) + if pending: + permit = acknowledged.entries[0].permit_artifact + chain = ProductionDeliveryPermitChain.build( + (), + pending=ProductionPendingDeliveryPermit.build( + permit, + receipt_absence_observed_at=request.result_loss_observed_at, + ), + ) + else: + chain = acknowledged + + result = _result_loss_closure_result(dispatch, marker, request, chain) + artifact, retained_chain = result.artifacts() + + assert retained_chain == chain + assert artifact.payload.closure_request_sha256 == request.request_sha256() + assert artifact.payload.pending_permit_count == (1 if pending else 0) + assert dispatch.lease_token not in request.canonical_bytes().decode("utf-8") + assert dispatch.delivery_authority_token not in request.canonical_bytes().decode( + "utf-8" + ) + invalid = result.model_dump(mode="json") + invalid["permit_chain_sha256"] = "f" * 64 + with pytest.raises(ValueError, match="closure response"): + hosted.ProductionDeliveryResultLossClosureResult.model_validate(invalid) + + +def test_unavailable_result_loss_fence_never_returns_a_callback_or_retries( + monkeypatch, tmp_path, config, sealed +) -> None: + workflow, _ = sealed + calls = 0 + + def runner(_argv, _run_dir, _child_env): + nonlocal calls + calls += 1 + raise RuntimeError("child result lost") + + adapter, dispatch = _prepared_adapter( + monkeypatch, tmp_path, config, workflow, runner + ) + monkeypatch.setattr( + adapter, + "_revalidate_terminal_authority", + lambda **_kwargs: (object(), object()), + ) + + def refuse_unsigned_result_loss(**_kwargs): + raise ValueError("hosted result-loss closure is unavailable") + + monkeypatch.setattr( + adapter, + "_produce_managed_result_loss", + refuse_unsigned_result_loss, + ) + authority = DeliveryAuthority( + dispatch.managed_delivery_authority_url, + dispatch.delivery_authority_token, + ) + with pytest.raises(RuntimeError, match="signed terminal reconciliation"): + adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run", + authority=authority, + closure_authority=SimpleNamespace(), + ) + + reservation = adapter._ledger.lookup( + f"{dispatch.tenant_id}:{dispatch.idempotency_key}" + ) + assert reservation is not None + assert reservation["outcome"] == "RECONCILIATION_REQUIRED" + with pytest.raises(RuntimeError, match="signed terminal reconciliation"): + adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run-again", + authority=authority, + closure_authority=SimpleNamespace(), + ) + assert calls == 1 + + def test_params_reference_resolves_only_from_protected_local_root( monkeypatch, tmp_path, config, sealed ) -> None: @@ -1302,17 +1908,24 @@ def test_safe_halt_callback_requires_signed_terminal_proof() -> None: ) -def test_terminal_callback_refuses_conflicting_uncertainty_state() -> None: - proof = sign_production_terminal_verification( - _reconciliation_payload(), _private_key() - ) +@pytest.mark.parametrize( + ("payload_factory", "uncertain_delivery"), + [ + (_reconciliation_payload, False), + (_acknowledged_reconciliation_payload, True), + ], +) +def test_terminal_callback_refuses_conflicting_uncertainty_state( + payload_factory, uncertain_delivery +) -> None: + proof = sign_production_terminal_verification(payload_factory(), _private_key()) with pytest.raises(ValueError, match="uncertainty conflicts"): HostedTerminalEvent( run_id=proof.payload.run_id, outcome="RECONCILIATION_REQUIRED", report_sha256=proof.payload.run_report_sha256, started=True, - uncertain_delivery=False, + uncertain_delivery=uncertain_delivery, terminal_verification_artifact_bytes_base64=b64encode( hosted.canonical_json(proof) ).decode("ascii"), @@ -1320,6 +1933,33 @@ def test_terminal_callback_refuses_conflicting_uncertainty_state() -> None: ) +@pytest.mark.parametrize( + ("payload_factory", "uncertain_delivery"), + [ + (_reconciliation_payload, True), + (_acknowledged_reconciliation_payload, False), + (_managed_result_loss_payload, True), + (_managed_result_loss_acknowledged_payload, False), + ], +) +def test_terminal_callback_derives_uncertainty_from_pending_permit_count( + payload_factory, uncertain_delivery +) -> None: + proof = sign_production_terminal_verification(payload_factory(), _private_key()) + terminal = HostedTerminalEvent( + run_id=proof.payload.run_id, + outcome="RECONCILIATION_REQUIRED", + report_sha256=proof.payload.run_report_sha256, + started=True, + uncertain_delivery=uncertain_delivery, + terminal_verification_artifact_bytes_base64=b64encode( + hosted.canonical_json(proof) + ).decode("ascii"), + terminal_verification_artifact_sha256=proof.artifact_sha256(), + ) + assert terminal.uncertain_delivery is uncertain_delivery + + def test_callback_rejects_confirmed_summary_for_reconciliation() -> None: proof = sign_production_terminal_verification( _reconciliation_payload(), _private_key() diff --git a/tests/test_terminal_verification_v2.py b/tests/test_terminal_verification_v2.py index 08107dbb..ed8195ff 100644 --- a/tests/test_terminal_verification_v2.py +++ b/tests/test_terminal_verification_v2.py @@ -17,6 +17,7 @@ EffectVerificationEvidence, ExecutionOutcomeEnvelope, IdentityCheck, + ManagedResultLossEvidence, OutcomeContractCounts, PostconditionContractEvidence, RunReport, @@ -25,13 +26,22 @@ ) from openadapt_flow.qualification_admission_v2 import canonical_json from openadapt_flow.receipt import RunReceipt +from openadapt_flow.runner.hosted_adapter import ( + ManagedChildStartEvidence, + ProductionDeliveryResultLossClosureRequest, + ProductionDeliveryResultLossClosureResult, +) from openadapt_flow.terminal_verification_v2 import ( + RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN, + RESULT_LOSS_CLOSURE_REQUEST_DOMAIN, SIGNATURE_DOMAIN, ProductionAuthorizationEvidenceManifest, ProductionDeliveryPermit, ProductionDeliveryPermitChain, ProductionDeliveryPermitPayload, ProductionDeliveryReceiptPayload, + ProductionDeliveryResultLossClosureArtifact, + ProductionDeliveryResultLossClosurePayload, ProductionEffectEvidence, ProductionEffectEvidenceManifest, ProductionEvidenceManifests, @@ -57,6 +67,7 @@ rebuild_production_delivery_permit_chain_from_artifacts, sign_production_delivery_permit, sign_production_delivery_receipt, + sign_production_delivery_result_loss_closure, sign_production_terminal_verification, verify_production_terminal_verification, ) @@ -761,6 +772,288 @@ def _reconciliation_payload() -> ProductionTerminalVerificationPayload: ) +def _acknowledged_reconciliation_payload() -> ProductionTerminalVerificationPayload: + pending_payload = _reconciliation_payload() + chain = _permit_chain() + assert chain.entries + payload = pending_payload.model_dump(mode="json") + payload.update( + { + "permit_chain": chain.model_dump(mode="json"), + "permit_count": 1, + "acknowledged_permit_count": 1, + "pending_permit_count": 0, + "final_authority_sequence": chain.entries[-1].authority_sequence, + "final_runtime_delivery_sequence": ( + chain.entries[-1].runtime_delivery_sequence + ), + "execution_authority_signer_sha256": ( + chain.entries[-1].authority_signer_sha256 + ), + "evidence_manifests": _reconciliation_manifests(chain).model_dump( + mode="json" + ), + "run_report_object_version": "version:reconciliation:acknowledged:1", + } + ) + return ProductionTerminalVerificationPayload.model_validate(payload) + + +def _result_loss_request() -> ProductionDeliveryResultLossClosureRequest: + marker = ManagedChildStartEvidence.create( + started_at="2026-08-18T12:00:00Z", + dispatch_id="00000000-0000-4000-8000-000000000009", + dispatch_session_id="00000000-0000-4000-8000-000000000010", + run_id=IDS["run_id"], + managed_dispatch_binding_sha256="sha256:" + SHA_C, + authenticated_runner_id_sha256=SHA_A, + authenticated_session_id_sha256=SHA_B, + execution_authority_id=IDS["execution_authority_id"], + execution_authority_sha256=SHA_A, + execution_authority_signer_sha256=(_permit_entry().authority_signer_sha256), + run_store_identity_sha256=SHA_B, + ) + return ProductionDeliveryResultLossClosureRequest( + child_start_evidence=marker, + result_loss_observed_at="2026-08-18T12:00:02Z", + ) + + +def _result_loss_closure( + chain: ProductionDeliveryPermitChain, +) -> ProductionDeliveryResultLossClosureArtifact: + request = _result_loss_request() + all_permits = (*chain.entries, *((chain.pending,) if chain.pending else ())) + assert all_permits + first = all_permits[0] + final = all_permits[-1] + pending = chain.pending + acknowledged = chain.entries[-1] if chain.entries else None + return sign_production_delivery_result_loss_closure( + ProductionDeliveryResultLossClosurePayload( + closure_id="00000000-0000-4000-8000-000000000011", + closure_request_sha256=request.request_sha256(), + closed_at="2026-08-18T12:00:02Z", + result_loss_observed_at="2026-08-18T12:00:02Z", + receipt_absence_observed_at=( + pending.receipt_absence_observed_at if pending else None + ), + tenant_id=IDS["tenant_id"], + run_id=IDS["run_id"], + flow_run_id_sha256=hashlib.sha256( + IDS["run_id"].encode("utf-8") + ).hexdigest(), + dispatch_id="00000000-0000-4000-8000-000000000009", + dispatch_session_id="00000000-0000-4000-8000-000000000010", + managed_dispatch_binding_sha256="sha256:" + SHA_C, + idempotency_key_sha256=SHA_D, + authenticated_runner_id_sha256=SHA_A, + authenticated_session_id_sha256=SHA_B, + execution_authority_id=IDS["execution_authority_id"], + execution_authority_sha256=SHA_A, + execution_authority_signer_sha256=final.authority_signer_sha256, + child_started_at="2026-08-18T12:00:00Z", + child_start_evidence_sha256=(request.child_start_evidence.marker_sha256), + run_store_identity_sha256=SHA_B, + permit_chain_sha256=chain.permit_chain_sha256, + permit_count=len(all_permits), + acknowledged_permit_count=len(chain.entries), + pending_permit_count=1 if pending else 0, + pending_permit_artifact_sha256=( + pending.permit_artifact_sha256 if pending else None + ), + run_request_sha256=first.run_request_sha256, + pending_action_request_sha256=( + pending.action_request_sha256 if pending else None + ), + final_input_edge_sequence=final.input_edge_sequence, + final_authority_sequence=final.authority_sequence, + final_runtime_delivery_sequence=( + acknowledged.runtime_delivery_sequence if acknowledged else 0 + ), + ), + _authority_key(), + ) + + +def _managed_result_loss_payload( + chain: ProductionDeliveryPermitChain | None = None, +) -> ProductionTerminalVerificationPayload: + chain = chain or _pending_permit_chain() + all_permits = (*chain.entries, *((chain.pending,) if chain.pending else ())) + assert all_permits + first = all_permits[0] + final = all_permits[-1] + pending = chain.pending + acknowledged = chain.entries[-1] if chain.entries else None + request = _result_loss_request() + closure = _result_loss_closure(chain) + loss = ManagedResultLossEvidence.create( + loss_code="report_missing", + child_started_at="2026-08-18T12:00:00Z", + child_start_evidence_sha256=request.child_start_evidence.marker_sha256, + run_store_identity_sha256=SHA_B, + observed_at=request.result_loss_observed_at, + run_id=IDS["run_id"], + flow_run_id_sha256=hashlib.sha256(IDS["run_id"].encode("utf-8")).hexdigest(), + dispatch_id="00000000-0000-4000-8000-000000000009", + dispatch_session_id="00000000-0000-4000-8000-000000000010", + managed_dispatch_binding_sha256="sha256:" + SHA_C, + idempotency_key_sha256=SHA_D, + authenticated_runner_id_sha256=SHA_A, + authenticated_session_id_sha256=SHA_B, + execution_authority_id=IDS["execution_authority_id"], + execution_authority_sha256=SHA_A, + execution_authority_signer_sha256=final.authority_signer_sha256, + delivery_result_loss_closure_artifact_sha256=closure.artifact_sha256(), + pending_permit_artifact_sha256=( + pending.permit_artifact_sha256 if pending else None + ), + run_request_sha256=first.run_request_sha256, + pending_action_request_sha256=( + pending.action_request_sha256 if pending else None + ), + ) + outcome = ProductionExecutionOutcome( + outcome="HALTED", + profile="standard", + production_eligible=False, + execution_completed=False, + required_contracts=TerminalContractCounts( + authorization=1, + identity=0, + postcondition=0, + effect=0, + ), + passed_contracts=TerminalContractCounts( + authorization=1, + identity=0, + postcondition=0, + effect=0, + ), + workflow_contract_sha256=SHA_A, + postcondition_evidence=(), + evidence_classes=("authorization",), + model_calls=0, + external_network_calls="none", + managed_result_loss_evidence_sha256=loss.evidence_sha256, + ) + receipt = ProductionRunReceipt( + source_schema_version="openadapt.run-report/v1", + outcome="HALTED", + transaction_outcome="RECONCILIATION_REQUIRED", + profile="standard", + production_eligible=False, + steps_total=1, + steps_ok=0, + heals=0, + model_calls=0, + est_cost_microusd=0, + duration_ms=0, + rung_histogram={}, + evidence_classes=("authorization",), + effect_tier_reached="none", + authorization_required=1, + authorization_confirmed=1, + identity_required=0, + identity_confirmed=0, + postconditions_required=0, + postconditions_confirmed=0, + effects_required=0, + effects_confirmed=0, + identity_armed=0, + identity_applicable=0, + over_halt_count=0, + substrate="web", + provenance="production", + receipt_builder_version="1.2.3", + external_network_calls="none", + bundle_digest=SHA_A, + source_receipt_digest=SHA_B, + source_receipt_sha256=SHA_B, + generated_at="2026-08-18T12:00:00Z", + managed_result_loss_evidence_sha256=loss.evidence_sha256, + ) + base = _halted_manifests(chain) + manifests = base.model_copy( + update={ + "identity": build_evidence_manifest( + ProductionIdentityEvidenceManifest, + identity_contract_sha256=SHA_D, + workflow_contract_sha256=SHA_A, + required=0, + confirmed=0, + results=(), + ), + "postcondition": build_evidence_manifest( + ProductionPostconditionEvidenceManifest, + workflow_contract_sha256=SHA_A, + required=0, + confirmed=0, + records=(), + ), + "effect": build_evidence_manifest( + ProductionEffectEvidenceManifest, + effect_contract_sha256=SHA_E, + workflow_contract_sha256=SHA_A, + required=0, + confirmed=0, + records=(), + ), + } + ) + return ProductionTerminalVerificationPayload( + **IDS, + flow_run_id_sha256=loss.flow_run_id_sha256, + bundle_artifact_sha256=SHA_B, + bundle_content_digest=SHA_A, + environment_digest=SHA_A, + environment_contract_sha256=SHA_B, + runtime_environment_sha256=SHA_C, + identity_contract_sha256=SHA_D, + effect_contract_sha256=SHA_E, + runtime_substrate="web", + admission_artifact_sha256=SHA_D, + admission_policy_sha256=SHA_A, + evidence_identity_sha256=SHA_E, + admitted_runtime_build_sha256=SHA_C, + evidence_runner_signer_sha256=evidence_runner_signer_sha256(_public_key()), + qualification_signer_registry_sha256=SHA_E, + qualification_signer_registry_revision=7, + execution_authority_sha256=SHA_A, + execution_authority_signer_sha256=final.authority_signer_sha256, + permit_chain=chain, + permit_count=len(all_permits), + acknowledged_permit_count=len(chain.entries), + pending_permit_count=1 if pending else 0, + final_authority_sequence=final.authority_sequence, + final_runtime_delivery_sequence=( + acknowledged.runtime_delivery_sequence if acknowledged else 0 + ), + workflow_contract_sha256=SHA_A, + execution_outcome=outcome, + execution_outcome_sha256=outcome.artifact_sha256(), + run_receipt=receipt, + run_receipt_sha256=hashlib.sha256( + canonical_json(receipt.model_dump(mode="json")) + ).hexdigest(), + run_report_sha256=SHA_B, + run_report_object_version="version:managed-result-loss:1", + run_report_object_sha256=SHA_B, + evidence_manifests=manifests, + managed_result_loss=loss, + delivery_result_loss_closure=closure, + verified_at="2026-08-18T12:00:02Z", + issued_at="2026-08-18T12:00:03Z", + ) + + +def _managed_result_loss_acknowledged_payload() -> ( + ProductionTerminalVerificationPayload +): + return _managed_result_loss_payload(_permit_chain()) + + def _payload() -> ProductionTerminalVerificationPayload: receipt = _receipt() chain = _permit_chain() @@ -869,6 +1162,8 @@ def _expected( run_report_object_version=payload.run_report_object_version, run_report_object_sha256=payload.run_report_object_sha256, evidence_manifests=payload.evidence_manifests, + managed_result_loss=payload.managed_result_loss, + delivery_result_loss_closure=payload.delivery_result_loss_closure, ) @@ -996,6 +1291,65 @@ def test_terminal_v2_signs_and_verifies_reconciliation_proof() -> None: assert record.observed_effect == "unknown" +def test_terminal_v2_signs_managed_result_loss_without_effect_absence() -> None: + payload = _managed_result_loss_payload() + envelope = sign_production_terminal_verification(payload, _private_key()) + loss = payload.managed_result_loss + + assert loss is not None + assert payload.run_receipt.transaction_outcome == "RECONCILIATION_REQUIRED" + assert payload.pending_permit_count == 1 + assert payload.evidence_manifests.effect.required == 0 + assert payload.evidence_manifests.effect.records == () + assert loss.delivery_state == "CLOSED_UNRESOLVED_RESULT_LOSS" + assert loss.child_report_retained is False + assert loss.effect_absence_claimed is False + assert loss.not_received_claimed is False + assert loss.blind_retry_authorized is False + assert loss.actuation_replay_authorized is False + assert envelope.payload.managed_result_loss == loss + + +def test_terminal_v2_signs_ack_won_managed_result_loss_without_uncertainty() -> None: + payload = _managed_result_loss_acknowledged_payload() + envelope = sign_production_terminal_verification(payload, _private_key()) + + digest = verify_production_terminal_verification( + envelope, + expected=_expected(payload), + now=NOW, + ) + + assert digest == envelope.artifact_sha256() + assert payload.acknowledged_permit_count == 1 + assert payload.pending_permit_count == 0 + assert payload.permit_chain.pending is None + assert payload.managed_result_loss is not None + assert payload.managed_result_loss.pending_permit_artifact_sha256 is None + assert payload.managed_result_loss.pending_action_request_sha256 is None + assert payload.evidence_manifests.effect.records == () + + +@pytest.mark.parametrize( + "field", + [ + "child_report_retained", + "effect_absence_claimed", + "not_received_claimed", + "blind_retry_authorized", + "actuation_replay_authorized", + ], +) +def test_managed_result_loss_refuses_a_positive_prohibited_claim(field: str) -> None: + loss = _managed_result_loss_payload().managed_result_loss + assert loss is not None + raw = loss.model_dump(mode="json") + raw[field] = True + + with pytest.raises(ValidationError): + ManagedResultLossEvidence.model_validate(raw) + + def test_pending_delivery_chain_rebuilds_from_exact_retained_permit() -> None: chain = _pending_permit_chain() assert chain.pending is not None @@ -1419,8 +1773,16 @@ def test_non_success_terminal_cross_language_vectors_are_exact() -> None: b64decode(fixture["private_key_base64"], validate=True) ) payloads = { + "verified-complete": _payload(), "halted-before-effect-zero-permit": _halted_payload(), "reconciliation-required-pending-permit": _reconciliation_payload(), + "reconciliation-required-acknowledged-inconclusive": ( + _acknowledged_reconciliation_payload() + ), + "reconciliation-required-managed-result-loss": (_managed_result_loss_payload()), + "reconciliation-required-managed-result-loss-acknowledged": ( + _managed_result_loss_acknowledged_payload() + ), } for vector in fixture["vectors"]: @@ -1440,12 +1802,15 @@ def test_non_success_terminal_cross_language_vectors_are_exact() -> None: envelope.artifact_sha256() == (vector["terminal_verification_artifact_sha256"]) ) + effect_records = envelope.payload.evidence_manifests.effect.records assert ( - envelope.payload.evidence_manifests.effect.records[0].model_dump( - mode="json" - ) - == vector["effect_state"] - ) + effect_records[0].model_dump(mode="json") if effect_records else None + ) == vector["effect_state"] + assert ( + envelope.payload.managed_result_loss.model_dump(mode="json") + if envelope.payload.managed_result_loss is not None + else None + ) == vector["managed_result_loss"] callback = vector["callback"] assert callback["run_id"] == payload.run_id assert callback["outcome"] == payload.run_receipt.transaction_outcome @@ -1453,6 +1818,50 @@ def test_non_success_terminal_cross_language_vectors_are_exact() -> None: assert callback["artifact_bytes_source"] == "envelope_canonical_base64" +def test_managed_result_loss_closure_cross_language_vector_is_exact() -> None: + fixture = json.loads( + Path("tests/fixtures/terminal_verification_v2_terminal_vectors.json").read_text( + encoding="utf-8" + ) + ) + vector = fixture["managed_result_loss_closure_vector"] + request_raw = b64decode(vector["request_canonical_base64"], validate=True) + closure_raw = b64decode( + vector["closure_artifact_canonical_base64"], validate=True + ) + chain_raw = b64decode(vector["permit_chain_canonical_base64"], validate=True) + result_raw = b64decode(vector["result_canonical_base64"], validate=True) + request = ProductionDeliveryResultLossClosureRequest.model_validate_json( + request_raw + ) + closure = ProductionDeliveryResultLossClosureArtifact.model_validate_json( + closure_raw + ) + chain = ProductionDeliveryPermitChain.model_validate_json(chain_raw) + result = ProductionDeliveryResultLossClosureResult.model_validate_json(result_raw) + + assert vector["http_method"] == "POST" + assert vector["http_route"] == ( + "/api/internal/managed-delivery-result-loss-closure" + ) + assert vector["authorization_credential_source"] == "hosted_dispatch.lease_token" + assert b64decode(vector["request_digest_domain_base64"], validate=True) == ( + RESULT_LOSS_CLOSURE_REQUEST_DOMAIN + ) + assert b64decode(vector["payload_signature_domain_base64"], validate=True) == ( + RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN + ) + assert request.canonical_bytes() == request_raw + assert request.request_sha256() == vector["request_sha256"] + assert closure.canonical_bytes() == closure_raw + assert closure.payload_sha256 == vector["closure_payload_sha256"] + assert closure.artifact_sha256() == vector["closure_artifact_sha256"] + assert canonical_json(chain) == chain_raw + assert chain.permit_chain_sha256 == vector["permit_chain_sha256"] + assert canonical_json(result) == result_raw + assert result.artifacts() == (closure, chain) + + def test_delivery_artifact_rebuild_rejects_noncanonical_stored_bytes() -> None: entry = _permit_chain().entries[0] with pytest.raises( From 134f6c8769a8df0c2a89c4aba56ea0d5677cc8de Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Thu, 27 Aug 2026 15:51:50 -0400 Subject: [PATCH 8/9] fix(hosted): bind result-loss delivery identity --- openadapt_flow/terminal_verification_v2.py | 14 +++++ public-artifacts.json | 2 +- ...inal_verification_v2_terminal_vectors.json | 48 +++++++-------- tests/test_terminal_verification_v2.py | 59 +++++++++++++++++-- 4 files changed, 92 insertions(+), 31 deletions(-) diff --git a/openadapt_flow/terminal_verification_v2.py b/openadapt_flow/terminal_verification_v2.py index 6d531024..abaa2dbf 100644 --- a/openadapt_flow/terminal_verification_v2.py +++ b/openadapt_flow/terminal_verification_v2.py @@ -2128,6 +2128,20 @@ def verify_production_delivery_result_loss_closure_binding( result_loss.run_store_identity_sha256, ): raise ValueError("result-loss closure identity binding is invalid") + if permit_chain.entries and any( + ( + item.authenticated_runner_id_sha256, + item.authenticated_session_id_sha256, + ) + != ( + payload.authenticated_runner_id_sha256, + payload.authenticated_session_id_sha256, + ) + for item in permit_chain.entries + ): + raise ValueError( + "result-loss closure authenticated delivery identity is invalid" + ) if ( payload.permit_chain_sha256, payload.permit_count, diff --git a/public-artifacts.json b/public-artifacts.json index d254dd80..69322eac 100644 --- a/public-artifacts.json +++ b/public-artifacts.json @@ -5781,7 +5781,7 @@ }, { "path": "tests/fixtures/terminal_verification_v2_terminal_vectors.json", - "sha256": "314c902ca5420d312626d521cec05652848f503e76226cb7168ed2384c759376" + "sha256": "0e94c1b106ca06ef6978c5923770b6b4ae32661deb68ce81078fede9a611dd5f" }, { "path": "tests/fixtures/theme-region-stable-overhalt-v1.16.1.json", diff --git a/tests/fixtures/terminal_verification_v2_terminal_vectors.json b/tests/fixtures/terminal_verification_v2_terminal_vectors.json index e175c30f..5b65c8fb 100644 --- a/tests/fixtures/terminal_verification_v2_terminal_vectors.json +++ b/tests/fixtures/terminal_verification_v2_terminal_vectors.json @@ -2,18 +2,18 @@ "key_id": "evidence-runner-ed25519-56475aa75463474c", "managed_result_loss_closure_vector": { "authorization_credential_source": "hosted_dispatch.lease_token", - "closure_artifact_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImF1dGhlbnRpY2F0ZWRfcnVubmVyX2lkX3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJhdXRoZW50aWNhdGVkX3Nlc3Npb25faWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImJsaW5kX3JldHJ5X2F1dGhvcml6ZWQiOmZhbHNlLCJjaGlsZF9zdGFydF9ldmlkZW5jZV9zaGEyNTYiOiI2NWE2YmVkOTIzZTQ1YTZhYTBjNjI0MzA0NWUwMzY0YzkwYWNlMDY0OGZlY2I1YjBiYjJlYmQ5NDAyNzkzYzc3IiwiY2hpbGRfc3RhcnRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiY2xvc2VkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJjbG9zdXJlX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDExIiwiY2xvc3VyZV9yZXF1ZXN0X3NoYTI1NiI6ImJhZjQxN2FlNDhlZWM0MDY1ODRlZjUyODZhYWExYjFhYzJlNmUwM2Q4YTI5YTRmYjRiMGM1MzI3MDRiZTU5YzYiLCJjbG9zdXJlX3NlcXVlbmNlIjoxLCJkZWxpdmVyeV9hY2tub3dsZWRnbWVudF9hdXRob3JpemVkIjpmYWxzZSwiZGVsaXZlcnlfc3RhdGUiOiJDTE9TRURfVU5SRVNPTFZFRF9SRVNVTFRfTE9TUyIsImRpc3BhdGNoX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA5IiwiZGlzcGF0Y2hfc2Vzc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsImVmZmVjdF9hYnNlbmNlX2NsYWltZWQiOmZhbHNlLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImZpbmFsX2F1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZmluYWxfaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiZmluYWxfcnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6MCwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW1wb3RlbmN5X2tleV9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuYWdlZF9kaXNwYXRjaF9iaW5kaW5nX3NoYTI1NiI6InNoYTI1NjpjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwibmV3X3Blcm1pdF9hdXRob3JpemVkIjpmYWxzZSwibm90X3JlY2VpdmVkX2NsYWltZWQiOmZhbHNlLCJwZW5kaW5nX2FjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJwZW5kaW5nX3Blcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5IiwicGVuZGluZ19wZXJtaXRfY291bnQiOjEsInBlcm1pdF9jaGFpbl9zaGEyNTYiOiI1OWIzOTk3MWUzMDUxMDljNTQyOTk0NWE1ZmFiNDY2MGIwOWY0MWU3YWMzZmZkOTJlMzUwNmFmMDAzMzdkYTk1IiwicGVybWl0X2NvdW50IjoxLCJyZWNlaXB0X2Fic2VuY2Vfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInJlc3VsdF9sb3NzX29ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwicnVuX3N0b3JlX2lkZW50aXR5X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtcGF5bG9hZC92MiIsInRlbmFudF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMiIsInRlcm1pbmFsX2NhbGxiYWNrX3JlcXVpcmVkIjp0cnVlfSwicGF5bG9hZF9zaGEyNTYiOiI4ZGI1OGY2YTA1ZjRlZjFmODI3NzY2MDBkNWUyYTEzMDY0NWEzZTEyYjUwZmVlNjgxMDg4NzRlZmYwOGY3ZWJmIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZXN1bHQtbG9zcy1jbG9zdXJlLWFydGlmYWN0L3YyIiwic2lnbmF0dXJlIjoiYWpDYXd4d1kxWWlBVTF3cDNrdXBZR2t6bmw3MlZBVHBsSFdnWTF1WDU3akVPRmpfNHMyX3ZYUGxVRUF5VW1wTmhwNHhvcVRYQlA4YzhPZTJKTGUzRHciLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImRlbGl2ZXJ5LWF1dGhvcml0eS1lZDI1NTE5LTI0ZjZlZDZhY2JmZTEwMDkiLCJwdWJsaWNfa2V5IjoiS2F5NjRVRzh5dkN5TGhxVTAwMEx4elllVW0wTC9oTElsNVM4a3lLV2JkYz0ifX0=", - "closure_artifact_sha256": "cec357acdb74f4e02dd1c3209a8ec880915c9830136e07260f3c1f631f8d52e8", - "closure_payload_sha256": "8db58f6a05f4ef1f82776600d5e2a130645a3e12b50fee68108874eff08f7ebf", + "closure_artifact_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImF1dGhlbnRpY2F0ZWRfcnVubmVyX2lkX3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJhdXRoZW50aWNhdGVkX3Nlc3Npb25faWRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImJsaW5kX3JldHJ5X2F1dGhvcml6ZWQiOmZhbHNlLCJjaGlsZF9zdGFydF9ldmlkZW5jZV9zaGEyNTYiOiJlYzY5MDc5MTg4NmFlMmNlMDBjNGU1MTE1NGMzZjEyMGVhNDQxOWVkMjY1MWUwNzNkYjU3MjQ5MThhM2IwOGM2IiwiY2hpbGRfc3RhcnRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiY2xvc2VkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJjbG9zdXJlX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDExIiwiY2xvc3VyZV9yZXF1ZXN0X3NoYTI1NiI6ImQ5MzBhYjFhYmMyYjJhMDRiODQ4Yjk1NWZmZGZmZmExZmJlMDY1NWE1NTMyNWUwNDg5YWUxMWVkODQyMDIyNjAiLCJjbG9zdXJlX3NlcXVlbmNlIjoxLCJkZWxpdmVyeV9hY2tub3dsZWRnbWVudF9hdXRob3JpemVkIjpmYWxzZSwiZGVsaXZlcnlfc3RhdGUiOiJDTE9TRURfVU5SRVNPTFZFRF9SRVNVTFRfTE9TUyIsImRpc3BhdGNoX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA5IiwiZGlzcGF0Y2hfc2Vzc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsImVmZmVjdF9hYnNlbmNlX2NsYWltZWQiOmZhbHNlLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImZpbmFsX2F1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZmluYWxfaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiZmluYWxfcnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6MCwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW1wb3RlbmN5X2tleV9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuYWdlZF9kaXNwYXRjaF9iaW5kaW5nX3NoYTI1NiI6InNoYTI1NjpjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwibmV3X3Blcm1pdF9hdXRob3JpemVkIjpmYWxzZSwibm90X3JlY2VpdmVkX2NsYWltZWQiOmZhbHNlLCJwZW5kaW5nX2FjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJwZW5kaW5nX3Blcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5IiwicGVuZGluZ19wZXJtaXRfY291bnQiOjEsInBlcm1pdF9jaGFpbl9zaGEyNTYiOiI1OWIzOTk3MWUzMDUxMDljNTQyOTk0NWE1ZmFiNDY2MGIwOWY0MWU3YWMzZmZkOTJlMzUwNmFmMDAzMzdkYTk1IiwicGVybWl0X2NvdW50IjoxLCJyZWNlaXB0X2Fic2VuY2Vfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInJlc3VsdF9sb3NzX29ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwicnVuX3N0b3JlX2lkZW50aXR5X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtcGF5bG9hZC92MiIsInRlbmFudF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMiIsInRlcm1pbmFsX2NhbGxiYWNrX3JlcXVpcmVkIjp0cnVlfSwicGF5bG9hZF9zaGEyNTYiOiJhODkxYWM0Y2JhNWZkZmMxN2QwM2MxZjgwMmEyZGY4YzExZmJmMGI2MmU0ZjA0YmU2N2YxYTVjNmU2OGQwYTUzIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZXN1bHQtbG9zcy1jbG9zdXJlLWFydGlmYWN0L3YyIiwic2lnbmF0dXJlIjoiZ2JzdDNuNUkzMEFBdXc2ajE3UkRFLUFlUWV5cXNKa1J1TWxyR0I3SklDTzhTbkx4ZUJIWkszNDVUYjdZY2NpVWY1SExfQjhWTnRKWkRUUndGaTNVRFEiLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImRlbGl2ZXJ5LWF1dGhvcml0eS1lZDI1NTE5LTI0ZjZlZDZhY2JmZTEwMDkiLCJwdWJsaWNfa2V5IjoiS2F5NjRVRzh5dkN5TGhxVTAwMEx4elllVW0wTC9oTElsNVM4a3lLV2JkYz0ifX0=", + "closure_artifact_sha256": "65d0d82d577a5323022b2afce1a26a216a52d21ef982e0540c266f3063fbc797", + "closure_payload_sha256": "a891ac4cba5fdfc17d03c1f802a2df8c11fbf0b62e4f04be67f1a5c6e68d0a53", "http_method": "POST", "http_route": "/api/internal/managed-delivery-result-loss-closure", "payload_signature_domain_base64": "b3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1wYXlsb2FkLnYyAA==", "permit_chain_canonical_base64": "eyJlbnRyaWVzIjpbXSwicGVuZGluZyI6eyJhY3R1YXRpb25fcmVwbGF5X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0IjpudWxsLCJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0X3NoYTI1NiI6bnVsbCwiZGVsaXZlcnlfc3RhdGUiOiJVTlJFU09MVkVEIiwicGVybWl0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJwZXJtaXRfaWQiOiJwZXJtaXQ6MSIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2NoZWNrZWRfYXQiOiIyMDI2LTA4LTE4VDExOjU5OjMwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2V4cGlyZXNfYXQiOiIyMDI2LTA4LTIwVDExOjAwOjAwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiJjODdmNTc1MWFhOWIyNzljY2UyM2FhMTc4YWZmOTU2ZTE5ZjY5MGZjZDJmOWY3MGUwZDk5OTc4MmE2OTBkNmI4Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiIwQnlIZ3FEeFFacm5nTElGLTZ0Wk9xOWxYYW1jQVFDMGRvVVRuZUtOUGVTLVN5b09iUXlPa0F1UEZLNVBZY056T2JaeGdzZ0FMZFdhSV9zYVVDcGxCZyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwicGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJyZWNlaXB0X2Fic2VuY2Vfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVuZGluZy1wZXJtaXQvdjIifSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi92MiJ9", "permit_chain_sha256": "59b39971e305109c5429945a5fab4660b09f41e7ac3ffd92e3506af00337da95", - "request_canonical_base64": "eyJjaGlsZF9zdGFydF9ldmlkZW5jZSI6eyJhdXRoZW50aWNhdGVkX3J1bm5lcl9pZF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYXV0aGVudGljYXRlZF9zZXNzaW9uX2lkX3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJkaXNwYXRjaF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOSIsImRpc3BhdGNoX3Nlc3Npb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTAiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsIm1hbmFnZWRfZGlzcGF0Y2hfYmluZGluZ19zaGEyNTYiOiJzaGEyNTY6Y2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsIm1hcmtlcl9zaGEyNTYiOiI2NWE2YmVkOTIzZTQ1YTZhYTBjNjI0MzA0NWUwMzY0YzkwYWNlMDY0OGZlY2I1YjBiYjJlYmQ5NDAyNzkzYzc3IiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3N0b3JlX2lkZW50aXR5X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5tYW5hZ2VkLWNoaWxkLXN0YXJ0L3YxIiwic3RhcnRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIn0sImV4cGVjdGVkX2Nsb3N1cmVfc2VxdWVuY2UiOjAsInJlc3VsdF9sb3NzX29ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtcmVxdWVzdC92MiJ9", + "request_canonical_base64": "eyJjaGlsZF9zdGFydF9ldmlkZW5jZSI6eyJhdXRoZW50aWNhdGVkX3J1bm5lcl9pZF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiYXV0aGVudGljYXRlZF9zZXNzaW9uX2lkX3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJkaXNwYXRjaF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOSIsImRpc3BhdGNoX3Nlc3Npb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTAiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsIm1hbmFnZWRfZGlzcGF0Y2hfYmluZGluZ19zaGEyNTYiOiJzaGEyNTY6Y2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsIm1hcmtlcl9zaGEyNTYiOiJlYzY5MDc5MTg4NmFlMmNlMDBjNGU1MTE1NGMzZjEyMGVhNDQxOWVkMjY1MWUwNzNkYjU3MjQ5MThhM2IwOGM2IiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3N0b3JlX2lkZW50aXR5X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5tYW5hZ2VkLWNoaWxkLXN0YXJ0L3YxIiwic3RhcnRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIn0sImV4cGVjdGVkX2Nsb3N1cmVfc2VxdWVuY2UiOjAsInJlc3VsdF9sb3NzX29ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtcmVxdWVzdC92MiJ9", "request_digest_domain_base64": "b3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1yZXF1ZXN0LnYyAA==", - "request_sha256": "baf417ae48eec406584ef5286aaa1b1ac2e6e03d8a29a4fb4b0c532704be59c6", - "result_canonical_base64": "eyJjbG9zdXJlX2FydGlmYWN0X2J5dGVzX2Jhc2U2NCI6ImV5SndZWGxzYjJGa0lqcDdJbUZqYTI1dmQyeGxaR2RsWkY5d1pYSnRhWFJmWTI5MWJuUWlPakFzSW1GamRIVmhkR2x2Ymw5eVpYQnNZWGxmWVhWMGFHOXlhWHBsWkNJNlptRnNjMlVzSW1GMWRHaGxiblJwWTJGMFpXUmZjblZ1Ym1WeVgybGtYM05vWVRJMU5pSTZJbUZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0VpTENKaGRYUm9aVzUwYVdOaGRHVmtYM05sYzNOcGIyNWZhV1JmYzJoaE1qVTJJam9pWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWWlJc0ltSnNhVzVrWDNKbGRISjVYMkYxZEdodmNtbDZaV1FpT21aaGJITmxMQ0pqYUdsc1pGOXpkR0Z5ZEY5bGRtbGtaVzVqWlY5emFHRXlOVFlpT2lJMk5XRTJZbVZrT1RJelpUUTFZVFpoWVRCak5qSTBNekEwTldVd016WTBZemt3WVdObE1EWTBPR1psWTJJMVlqQmlZakpsWW1RNU5EQXlOemt6WXpjM0lpd2lZMmhwYkdSZmMzUmhjblJsWkY5aGRDSTZJakl3TWpZdE1EZ3RNVGhVTVRJNk1EQTZNREJhSWl3aVkyeHZjMlZrWDJGMElqb2lNakF5Tmkwd09DMHhPRlF4TWpvd01Eb3dNbG9pTENKamJHOXpkWEpsWDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01ERXhJaXdpWTJ4dmMzVnlaVjl5WlhGMVpYTjBYM05vWVRJMU5pSTZJbUpoWmpReE4yRmxORGhsWldNME1EWTFPRFJsWmpVeU9EWmhZV0V4WWpGaFl6SmxObVV3TTJRNFlUSTVZVFJtWWpSaU1HTTFNekkzTURSaVpUVTVZellpTENKamJHOXpkWEpsWDNObGNYVmxibU5sSWpveExDSmtaV3hwZG1WeWVWOWhZMnR1YjNkc1pXUm5iV1Z1ZEY5aGRYUm9iM0pwZW1Wa0lqcG1ZV3h6WlN3aVpHVnNhWFpsY25sZmMzUmhkR1VpT2lKRFRFOVRSVVJmVlU1U1JWTlBURlpGUkY5U1JWTlZURlJmVEU5VFV5SXNJbVJwYzNCaGRHTm9YMmxrSWpvaU1EQXdNREF3TURBdE1EQXdNQzAwTURBd0xUZ3dNREF0TURBd01EQXdNREF3TURBNUlpd2laR2x6Y0dGMFkyaGZjMlZ6YzJsdmJsOXBaQ0k2SWpBd01EQXdNREF3TFRBd01EQXROREF3TUMwNE1EQXdMVEF3TURBd01EQXdNREF4TUNJc0ltVm1abVZqZEY5aFluTmxibU5sWDJOc1lXbHRaV1FpT21aaGJITmxMQ0psZUdWamRYUnBiMjVmWVhWMGFHOXlhWFI1WDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01EQTRJaXdpWlhobFkzVjBhVzl1WDJGMWRHaHZjbWwwZVY5emFHRXlOVFlpT2lKaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaElpd2laWGhsWTNWMGFXOXVYMkYxZEdodmNtbDBlVjl6YVdkdVpYSmZjMmhoTWpVMklqb2lNalJtTm1Wa05tRmpZbVpsTVRBd09XTXdNekJrTjJOaE5UWTNZek16WTJFME9ETXdPVEV4TkRrNE1qTTJZalUxTmpGaE5tTTRNbUZpWldNMVpHVXlPQ0lzSW1acGJtRnNYMkYxZEdodmNtbDBlVjl6WlhGMVpXNWpaU0k2TUN3aVptbHVZV3hmYVc1d2RYUmZaV1JuWlY5elpYRjFaVzVqWlNJNk1Td2labWx1WVd4ZmNuVnVkR2x0WlY5a1pXeHBkbVZ5ZVY5elpYRjFaVzVqWlNJNk1Dd2labXh2ZDE5eWRXNWZhV1JmYzJoaE1qVTJJam9pTVRGbE5UazBaalE0TVRrMU9HTXhNR1V6TURFMVpEQmlaakEwTkRkaE1qSm1NRFk0WVRoaE5qUTNaalEzTldSbU1UVmpaVEpqTjJGaU5HSTRaak5tTVNJc0ltbGtaVzF3YjNSbGJtTjVYMnRsZVY5emFHRXlOVFlpT2lKa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa0lpd2liV0Z1WVdkbFpGOWthWE53WVhSamFGOWlhVzVrYVc1blgzTm9ZVEkxTmlJNkluTm9ZVEkxTmpwalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOaklpd2libVYzWDNCbGNtMXBkRjloZFhSb2IzSnBlbVZrSWpwbVlXeHpaU3dpYm05MFgzSmxZMlZwZG1Wa1gyTnNZV2x0WldRaU9tWmhiSE5sTENKd1pXNWthVzVuWDJGamRHbHZibDl5WlhGMVpYTjBYM05vWVRJMU5pSTZJbVJrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1FpTENKd1pXNWthVzVuWDNCbGNtMXBkRjloY25ScFptRmpkRjl6YUdFeU5UWWlPaUk1TnpKaE9ESmhaalExTm1SaE5UUXlZbUZqTnpnME5qQXhPRGc1TmpZNE9Ea3haRGhpTjJOalpESTBNamt3TkRneE5tSXpNamt5WlRFMU4yVTFNamc1SWl3aWNHVnVaR2x1WjE5d1pYSnRhWFJmWTI5MWJuUWlPakVzSW5CbGNtMXBkRjlqYUdGcGJsOXphR0V5TlRZaU9pSTFPV0l6T1RrM01XVXpNRFV4TURsak5UUXlPVGswTldFMVptRmlORFkyTUdJd09XWTBNV1UzWVdNelptWmtPVEpsTXpVd05tRm1NREF6TXpka1lUazFJaXdpY0dWeWJXbDBYMk52ZFc1MElqb3hMQ0p5WldObGFYQjBYMkZpYzJWdVkyVmZiMkp6WlhKMlpXUmZZWFFpT2lJeU1ESTJMVEE0TFRFNFZERXlPakF3T2pBeVdpSXNJbkpsYzNWc2RGOXNiM056WDI5aWMyVnlkbVZrWDJGMElqb2lNakF5Tmkwd09DMHhPRlF4TWpvd01Eb3dNbG9pTENKeWRXNWZhV1FpT2lJd01EQXdNREF3TUMwd01EQXdMVFF3TURBdE9EQXdNQzB3TURBd01EQXdNREF3TURFaUxDSnlkVzVmY21WeGRXVnpkRjl6YUdFeU5UWWlPaUpqWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qSWl3aWNuVnVYM04wYjNKbFgybGtaVzUwYVhSNVgzTm9ZVEkxTmlJNkltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSWlMQ0p6WTJobGJXRmZkbVZ5YzJsdmJpSTZJbTl3Wlc1aFpHRndkQzV3Y205a2RXTjBhVzl1TFdSbGJHbDJaWEo1TFhKbGMzVnNkQzFzYjNOekxXTnNiM04xY21VdGNHRjViRzloWkM5Mk1pSXNJblJsYm1GdWRGOXBaQ0k2SWpBd01EQXdNREF3TFRBd01EQXROREF3TUMwNE1EQXdMVEF3TURBd01EQXdNREF3TWlJc0luUmxjbTFwYm1Gc1gyTmhiR3hpWVdOclgzSmxjWFZwY21Wa0lqcDBjblZsZlN3aWNHRjViRzloWkY5emFHRXlOVFlpT2lJNFpHSTFPR1kyWVRBMVpqUmxaakZtT0RJM056WTJNREJrTldVeVlURXpNRFkwTldFelpURXlZalV3Wm1WbE5qZ3hNRGc0TnpSbFptWXdPR1kzWldKbUlpd2ljMk5vWlcxaFgzWmxjbk5wYjI0aU9pSnZjR1Z1WVdSaGNIUXVjSEp2WkhWamRHbHZiaTFrWld4cGRtVnllUzF5WlhOMWJIUXRiRzl6Y3kxamJHOXpkWEpsTFdGeWRHbG1ZV04wTDNZeUlpd2ljMmxuYm1GMGRYSmxJam9pWVdwRFlYZDRkMWt4V1dsQlZURjNjRE5yZFhCWlIydDZibXczTWxaQlZIQnNTRmRuV1RGMVdEVTNha1ZQUm1wZk5ITXlYM1pZVUd4VlJVRjVWVzF3VG1od05IaHZjVlJZUWxBNFl6aFBaVEpLVEdVelJIY2lMQ0p6YVdkdVpYSWlPbnNpWVd4bmIzSnBkR2h0SWpvaVpXUXlOVFV4T1NJc0ltdGxlVjlwWkNJNkltUmxiR2wyWlhKNUxXRjFkR2h2Y21sMGVTMWxaREkxTlRFNUxUSTBaalpsWkRaaFkySm1aVEV3TURraUxDSndkV0pzYVdOZmEyVjVJam9pUzJGNU5qUlZSemg1ZGtONVRHaHhWVEF3TUV4NGVsbGxWVzB3VEM5b1RFbHNOVk00YTNsTFYySmtZejBpZlgwPSIsImNsb3N1cmVfYXJ0aWZhY3Rfc2hhMjU2IjoiY2VjMzU3YWNkYjc0ZjRlMDJkZDFjMzIwOWE4ZWM4ODA5MTVjOTgzMDEzNmUwNzI2MGYzYzFmNjMxZjhkNTJlOCIsInBlcm1pdF9jaGFpbl9ieXRlc19iYXNlNjQiOiJleUpsYm5SeWFXVnpJanBiWFN3aWNHVnVaR2x1WnlJNmV5SmhZM1IxWVhScGIyNWZjbVZ3YkdGNVgyRjFkR2h2Y21sNlpXUWlPbVpoYkhObExDSmtaV3hwZG1WeWVWOXlaV05sYVhCMFgyRnlkR2xtWVdOMElqcHVkV3hzTENKa1pXeHBkbVZ5ZVY5eVpXTmxhWEIwWDJGeWRHbG1ZV04wWDNOb1lUSTFOaUk2Ym5Wc2JDd2laR1ZzYVhabGNubGZjM1JoZEdVaU9pSlZUbEpGVTA5TVZrVkVJaXdpY0dWeWJXbDBYMkZ5ZEdsbVlXTjBJanA3SW5CaGVXeHZZV1FpT25zaVlXTjBhVzl1WDNKbGNYVmxjM1JmYzJoaE1qVTJJam9pWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkNJc0ltRmtiV2x6YzJsdmJsOWhjblJwWm1GamRGOXphR0V5TlRZaU9pSmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtJaXdpWVhWMGFHOXlhWFI1WDNObGNYVmxibU5sSWpvd0xDSmxiblpwY205dWJXVnVkRjlrYVdkbGMzUWlPaUpoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoSWl3aVpYWnBaR1Z1WTJWZmFXUmxiblJwZEhsZmMyaGhNalUySWpvaVpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpTSXNJbVY0WldOMWRHbHZibDloZFhSb2IzSnBkSGxmYVdRaU9pSXdNREF3TURBd01DMHdNREF3TFRRd01EQXRPREF3TUMwd01EQXdNREF3TURBd01EZ2lMQ0psZUdWamRYUnBiMjVmWVhWMGFHOXlhWFI1WDNOb1lUSTFOaUk2SW1GaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdFaUxDSm1iRzkzWDNKMWJsOXBaRjl6YUdFeU5UWWlPaUl4TVdVMU9UUm1ORGd4T1RVNFl6RXdaVE13TVRWa01HSm1NRFEwTjJFeU1tWXdOamhoT0dFMk5EZG1ORGMxWkdZeE5XTmxNbU0zWVdJMFlqaG1NMll4SWl3aWFXNXdkWFJmWldSblpWOXpaWEYxWlc1alpTSTZNU3dpYVhOemRXVmtYMkYwSWpvaU1qQXlOaTB3T0MweE9GUXhNam93TURvd01Gb2lMQ0p3WlhKdGFYUmZhV1FpT2lKd1pYSnRhWFE2TVNJc0luRjFZV3hwWm1sallYUnBiMjVmYzJsbmJtVnlYM0psWjJsemRISjVYMk5vWldOclpXUmZZWFFpT2lJeU1ESTJMVEE0TFRFNFZERXhPalU1T2pNd1dpSXNJbkYxWVd4cFptbGpZWFJwYjI1ZmMybG5ibVZ5WDNKbFoybHpkSEo1WDJWNGNHbHlaWE5mWVhRaU9pSXlNREkyTFRBNExUSXdWREV4T2pBd09qQXdXaUlzSW5GMVlXeHBabWxqWVhScGIyNWZjMmxuYm1WeVgzSmxaMmx6ZEhKNVgzSmxkbWx6YVc5dUlqbzNMQ0p4ZFdGc2FXWnBZMkYwYVc5dVgzTnBaMjVsY2w5eVpXZHBjM1J5ZVY5emFHRXlOVFlpT2lKbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbElpd2ljblZ1WDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01EQXhJaXdpY25WdVgzSmxjWFZsYzNSZmMyaGhNalUySWpvaVkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOall5SXNJbk5qYUdWdFlWOTJaWEp6YVc5dUlqb2liM0JsYm1Ga1lYQjBMbkJ5YjJSMVkzUnBiMjR0WkdWc2FYWmxjbmt0Y0dWeWJXbDBMWEJoZVd4dllXUXZkaklpZlN3aWNHRjViRzloWkY5emFHRXlOVFlpT2lKak9EZG1OVGMxTVdGaE9XSXlOemxqWTJVeU0yRmhNVGM0WVdabU9UVTJaVEU1WmpZNU1HWmpaREptT1dZM01HVXdaRGs1T1RjNE1tRTJPVEJrTm1JNElpd2ljMk5vWlcxaFgzWmxjbk5wYjI0aU9pSnZjR1Z1WVdSaGNIUXVjSEp2WkhWamRHbHZiaTFrWld4cGRtVnllUzF3WlhKdGFYUXRZWEowYVdaaFkzUXZkaklpTENKemFXZHVZWFIxY21VaU9pSXdRbmxJWjNGRWVGRmFjbTVuVEVsR0xUWjBXazl4T1d4WVlXMWpRVkZETUdSdlZWUnVaVXRPVUdWVExWTjViMDlpVVhsUGEwRjFVRVpMTlZCWlkwNTZUMkphZUdkelowRk1aRmRoU1Y5ellWVkRjR3hDWnlJc0luTnBaMjVsY2lJNmV5SmhiR2R2Y21sMGFHMGlPaUpsWkRJMU5URTVJaXdpYTJWNVgybGtJam9pWkdWc2FYWmxjbmt0WVhWMGFHOXlhWFI1TFdWa01qVTFNVGt0TWpSbU5tVmtObUZqWW1abE1UQXdPU0lzSW5CMVlteHBZMTlyWlhraU9pSkxZWGsyTkZWSE9IbDJRM2xNYUhGVk1EQXdUSGg2V1dWVmJUQk1MMmhNU1d3MVV6aHJlVXRYWW1SalBTSjlmU3dpY0dWeWJXbDBYMkZ5ZEdsbVlXTjBYM05vWVRJMU5pSTZJamszTW1FNE1tRm1ORFUyWkdFMU5ESmlZV00zT0RRMk1ERTRPRGsyTmpnNE9URmtPR0kzWTJOa01qUXlPVEEwT0RFMllqTXlPVEpsTVRVM1pUVXlPRGtpTENKeVpXTmxhWEIwWDJGaWMyVnVZMlZmYjJKelpYSjJaV1JmWVhRaU9pSXlNREkyTFRBNExURTRWREV5T2pBd09qQXlXaUlzSW5OamFHVnRZVjkyWlhKemFXOXVJam9pYjNCbGJtRmtZWEIwTG5CeWIyUjFZM1JwYjI0dFpHVnNhWFpsY25rdGNHVnVaR2x1Wnkxd1pYSnRhWFF2ZGpJaWZTd2ljR1Z5YldsMFgyTm9ZV2x1WDNOb1lUSTFOaUk2SWpVNVlqTTVPVGN4WlRNd05URXdPV00xTkRJNU9UUTFZVFZtWVdJME5qWXdZakE1WmpReFpUZGhZek5tWm1RNU1tVXpOVEEyWVdZd01ETXpOMlJoT1RVaUxDSnpZMmhsYldGZmRtVnljMmx2YmlJNkltOXdaVzVoWkdGd2RDNXdjbTlrZFdOMGFXOXVMV1JsYkdsMlpYSjVMWEJsY20xcGRDMWphR0ZwYmk5Mk1pSjkiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTliMzk5NzFlMzA1MTA5YzU0Mjk5NDVhNWZhYjQ2NjBiMDlmNDFlN2FjM2ZmZDkyZTM1MDZhZjAwMzM3ZGE5NSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1yZXN1bHQvdjIiLCJzdGF0dXMiOiJjbG9zZWQifQ==" + "request_sha256": "d930ab1abc2b2a04b848b955ffdfffa1fbe0655a55325e0489ae11ed84202260", + "result_canonical_base64": "eyJjbG9zdXJlX2FydGlmYWN0X2J5dGVzX2Jhc2U2NCI6ImV5SndZWGxzYjJGa0lqcDdJbUZqYTI1dmQyeGxaR2RsWkY5d1pYSnRhWFJmWTI5MWJuUWlPakFzSW1GamRIVmhkR2x2Ymw5eVpYQnNZWGxmWVhWMGFHOXlhWHBsWkNJNlptRnNjMlVzSW1GMWRHaGxiblJwWTJGMFpXUmZjblZ1Ym1WeVgybGtYM05vWVRJMU5pSTZJbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUlpTENKaGRYUm9aVzUwYVdOaGRHVmtYM05sYzNOcGIyNWZhV1JmYzJoaE1qVTJJam9pWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWXlJc0ltSnNhVzVrWDNKbGRISjVYMkYxZEdodmNtbDZaV1FpT21aaGJITmxMQ0pqYUdsc1pGOXpkR0Z5ZEY5bGRtbGtaVzVqWlY5emFHRXlOVFlpT2lKbFl6WTVNRGM1TVRnNE5tRmxNbU5sTURCak5HVTFNVEUxTkdNelpqRXlNR1ZoTkRReE9XVmtNalkxTVdVd056TmtZalUzTWpRNU1UaGhNMkl3T0dNMklpd2lZMmhwYkdSZmMzUmhjblJsWkY5aGRDSTZJakl3TWpZdE1EZ3RNVGhVTVRJNk1EQTZNREJhSWl3aVkyeHZjMlZrWDJGMElqb2lNakF5Tmkwd09DMHhPRlF4TWpvd01Eb3dNbG9pTENKamJHOXpkWEpsWDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01ERXhJaXdpWTJ4dmMzVnlaVjl5WlhGMVpYTjBYM05vWVRJMU5pSTZJbVE1TXpCaFlqRmhZbU15WWpKaE1EUmlPRFE0WWprMU5XWm1aR1ptWm1FeFptSmxNRFkxTldFMU5UTXlOV1V3TkRnNVlXVXhNV1ZrT0RReU1ESXlOakFpTENKamJHOXpkWEpsWDNObGNYVmxibU5sSWpveExDSmtaV3hwZG1WeWVWOWhZMnR1YjNkc1pXUm5iV1Z1ZEY5aGRYUm9iM0pwZW1Wa0lqcG1ZV3h6WlN3aVpHVnNhWFpsY25sZmMzUmhkR1VpT2lKRFRFOVRSVVJmVlU1U1JWTlBURlpGUkY5U1JWTlZURlJmVEU5VFV5SXNJbVJwYzNCaGRHTm9YMmxrSWpvaU1EQXdNREF3TURBdE1EQXdNQzAwTURBd0xUZ3dNREF0TURBd01EQXdNREF3TURBNUlpd2laR2x6Y0dGMFkyaGZjMlZ6YzJsdmJsOXBaQ0k2SWpBd01EQXdNREF3TFRBd01EQXROREF3TUMwNE1EQXdMVEF3TURBd01EQXdNREF4TUNJc0ltVm1abVZqZEY5aFluTmxibU5sWDJOc1lXbHRaV1FpT21aaGJITmxMQ0psZUdWamRYUnBiMjVmWVhWMGFHOXlhWFI1WDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01EQTRJaXdpWlhobFkzVjBhVzl1WDJGMWRHaHZjbWwwZVY5emFHRXlOVFlpT2lKaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaElpd2laWGhsWTNWMGFXOXVYMkYxZEdodmNtbDBlVjl6YVdkdVpYSmZjMmhoTWpVMklqb2lNalJtTm1Wa05tRmpZbVpsTVRBd09XTXdNekJrTjJOaE5UWTNZek16WTJFME9ETXdPVEV4TkRrNE1qTTJZalUxTmpGaE5tTTRNbUZpWldNMVpHVXlPQ0lzSW1acGJtRnNYMkYxZEdodmNtbDBlVjl6WlhGMVpXNWpaU0k2TUN3aVptbHVZV3hmYVc1d2RYUmZaV1JuWlY5elpYRjFaVzVqWlNJNk1Td2labWx1WVd4ZmNuVnVkR2x0WlY5a1pXeHBkbVZ5ZVY5elpYRjFaVzVqWlNJNk1Dd2labXh2ZDE5eWRXNWZhV1JmYzJoaE1qVTJJam9pTVRGbE5UazBaalE0TVRrMU9HTXhNR1V6TURFMVpEQmlaakEwTkRkaE1qSm1NRFk0WVRoaE5qUTNaalEzTldSbU1UVmpaVEpqTjJGaU5HSTRaak5tTVNJc0ltbGtaVzF3YjNSbGJtTjVYMnRsZVY5emFHRXlOVFlpT2lKa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa0lpd2liV0Z1WVdkbFpGOWthWE53WVhSamFGOWlhVzVrYVc1blgzTm9ZVEkxTmlJNkluTm9ZVEkxTmpwalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOaklpd2libVYzWDNCbGNtMXBkRjloZFhSb2IzSnBlbVZrSWpwbVlXeHpaU3dpYm05MFgzSmxZMlZwZG1Wa1gyTnNZV2x0WldRaU9tWmhiSE5sTENKd1pXNWthVzVuWDJGamRHbHZibDl5WlhGMVpYTjBYM05vWVRJMU5pSTZJbVJrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1FpTENKd1pXNWthVzVuWDNCbGNtMXBkRjloY25ScFptRmpkRjl6YUdFeU5UWWlPaUk1TnpKaE9ESmhaalExTm1SaE5UUXlZbUZqTnpnME5qQXhPRGc1TmpZNE9Ea3haRGhpTjJOalpESTBNamt3TkRneE5tSXpNamt5WlRFMU4yVTFNamc1SWl3aWNHVnVaR2x1WjE5d1pYSnRhWFJmWTI5MWJuUWlPakVzSW5CbGNtMXBkRjlqYUdGcGJsOXphR0V5TlRZaU9pSTFPV0l6T1RrM01XVXpNRFV4TURsak5UUXlPVGswTldFMVptRmlORFkyTUdJd09XWTBNV1UzWVdNelptWmtPVEpsTXpVd05tRm1NREF6TXpka1lUazFJaXdpY0dWeWJXbDBYMk52ZFc1MElqb3hMQ0p5WldObGFYQjBYMkZpYzJWdVkyVmZiMkp6WlhKMlpXUmZZWFFpT2lJeU1ESTJMVEE0TFRFNFZERXlPakF3T2pBeVdpSXNJbkpsYzNWc2RGOXNiM056WDI5aWMyVnlkbVZrWDJGMElqb2lNakF5Tmkwd09DMHhPRlF4TWpvd01Eb3dNbG9pTENKeWRXNWZhV1FpT2lJd01EQXdNREF3TUMwd01EQXdMVFF3TURBdE9EQXdNQzB3TURBd01EQXdNREF3TURFaUxDSnlkVzVmY21WeGRXVnpkRjl6YUdFeU5UWWlPaUpqWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qSWl3aWNuVnVYM04wYjNKbFgybGtaVzUwYVhSNVgzTm9ZVEkxTmlJNkltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSWlMQ0p6WTJobGJXRmZkbVZ5YzJsdmJpSTZJbTl3Wlc1aFpHRndkQzV3Y205a2RXTjBhVzl1TFdSbGJHbDJaWEo1TFhKbGMzVnNkQzFzYjNOekxXTnNiM04xY21VdGNHRjViRzloWkM5Mk1pSXNJblJsYm1GdWRGOXBaQ0k2SWpBd01EQXdNREF3TFRBd01EQXROREF3TUMwNE1EQXdMVEF3TURBd01EQXdNREF3TWlJc0luUmxjbTFwYm1Gc1gyTmhiR3hpWVdOclgzSmxjWFZwY21Wa0lqcDBjblZsZlN3aWNHRjViRzloWkY5emFHRXlOVFlpT2lKaE9Ea3hZV00wWTJKaE5XWmtabU14TjJRd00yTXhaamd3TW1FeVpHWTRZekV4Wm1KbU1HSTJNbVUwWmpBMFltVTJOMll4WVRWak5tVTJPR1F3WVRVeklpd2ljMk5vWlcxaFgzWmxjbk5wYjI0aU9pSnZjR1Z1WVdSaGNIUXVjSEp2WkhWamRHbHZiaTFrWld4cGRtVnllUzF5WlhOMWJIUXRiRzl6Y3kxamJHOXpkWEpsTFdGeWRHbG1ZV04wTDNZeUlpd2ljMmxuYm1GMGRYSmxJam9pWjJKemRETnVOVWt6TUVGQmRYYzJhakUzVWtSRkxVRmxVV1Y1Y1hOS2ExSjFUV3h5UjBJM1NrbERUemhUYmt4NFpVSklXa3N6TkRWVVlqZFpZMk5wVldZMVNFeGZRamhXVG5SS1drUlVVbmRHYVROVlJGRWlMQ0p6YVdkdVpYSWlPbnNpWVd4bmIzSnBkR2h0SWpvaVpXUXlOVFV4T1NJc0ltdGxlVjlwWkNJNkltUmxiR2wyWlhKNUxXRjFkR2h2Y21sMGVTMWxaREkxTlRFNUxUSTBaalpsWkRaaFkySm1aVEV3TURraUxDSndkV0pzYVdOZmEyVjVJam9pUzJGNU5qUlZSemg1ZGtONVRHaHhWVEF3TUV4NGVsbGxWVzB3VEM5b1RFbHNOVk00YTNsTFYySmtZejBpZlgwPSIsImNsb3N1cmVfYXJ0aWZhY3Rfc2hhMjU2IjoiNjVkMGQ4MmQ1NzdhNTMyMzAyMmIyYWZjZTFhMjZhMjE2YTUyZDIxZWY5ODJlMDU0MGMyNjZmMzA2M2ZiYzc5NyIsInBlcm1pdF9jaGFpbl9ieXRlc19iYXNlNjQiOiJleUpsYm5SeWFXVnpJanBiWFN3aWNHVnVaR2x1WnlJNmV5SmhZM1IxWVhScGIyNWZjbVZ3YkdGNVgyRjFkR2h2Y21sNlpXUWlPbVpoYkhObExDSmtaV3hwZG1WeWVWOXlaV05sYVhCMFgyRnlkR2xtWVdOMElqcHVkV3hzTENKa1pXeHBkbVZ5ZVY5eVpXTmxhWEIwWDJGeWRHbG1ZV04wWDNOb1lUSTFOaUk2Ym5Wc2JDd2laR1ZzYVhabGNubGZjM1JoZEdVaU9pSlZUbEpGVTA5TVZrVkVJaXdpY0dWeWJXbDBYMkZ5ZEdsbVlXTjBJanA3SW5CaGVXeHZZV1FpT25zaVlXTjBhVzl1WDNKbGNYVmxjM1JmYzJoaE1qVTJJam9pWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkNJc0ltRmtiV2x6YzJsdmJsOWhjblJwWm1GamRGOXphR0V5TlRZaU9pSmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtJaXdpWVhWMGFHOXlhWFI1WDNObGNYVmxibU5sSWpvd0xDSmxiblpwY205dWJXVnVkRjlrYVdkbGMzUWlPaUpoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoSWl3aVpYWnBaR1Z1WTJWZmFXUmxiblJwZEhsZmMyaGhNalUySWpvaVpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpTSXNJbVY0WldOMWRHbHZibDloZFhSb2IzSnBkSGxmYVdRaU9pSXdNREF3TURBd01DMHdNREF3TFRRd01EQXRPREF3TUMwd01EQXdNREF3TURBd01EZ2lMQ0psZUdWamRYUnBiMjVmWVhWMGFHOXlhWFI1WDNOb1lUSTFOaUk2SW1GaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdFaUxDSm1iRzkzWDNKMWJsOXBaRjl6YUdFeU5UWWlPaUl4TVdVMU9UUm1ORGd4T1RVNFl6RXdaVE13TVRWa01HSm1NRFEwTjJFeU1tWXdOamhoT0dFMk5EZG1ORGMxWkdZeE5XTmxNbU0zWVdJMFlqaG1NMll4SWl3aWFXNXdkWFJmWldSblpWOXpaWEYxWlc1alpTSTZNU3dpYVhOemRXVmtYMkYwSWpvaU1qQXlOaTB3T0MweE9GUXhNam93TURvd01Gb2lMQ0p3WlhKdGFYUmZhV1FpT2lKd1pYSnRhWFE2TVNJc0luRjFZV3hwWm1sallYUnBiMjVmYzJsbmJtVnlYM0psWjJsemRISjVYMk5vWldOclpXUmZZWFFpT2lJeU1ESTJMVEE0TFRFNFZERXhPalU1T2pNd1dpSXNJbkYxWVd4cFptbGpZWFJwYjI1ZmMybG5ibVZ5WDNKbFoybHpkSEo1WDJWNGNHbHlaWE5mWVhRaU9pSXlNREkyTFRBNExUSXdWREV4T2pBd09qQXdXaUlzSW5GMVlXeHBabWxqWVhScGIyNWZjMmxuYm1WeVgzSmxaMmx6ZEhKNVgzSmxkbWx6YVc5dUlqbzNMQ0p4ZFdGc2FXWnBZMkYwYVc5dVgzTnBaMjVsY2w5eVpXZHBjM1J5ZVY5emFHRXlOVFlpT2lKbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbElpd2ljblZ1WDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01EQXhJaXdpY25WdVgzSmxjWFZsYzNSZmMyaGhNalUySWpvaVkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOall5SXNJbk5qYUdWdFlWOTJaWEp6YVc5dUlqb2liM0JsYm1Ga1lYQjBMbkJ5YjJSMVkzUnBiMjR0WkdWc2FYWmxjbmt0Y0dWeWJXbDBMWEJoZVd4dllXUXZkaklpZlN3aWNHRjViRzloWkY5emFHRXlOVFlpT2lKak9EZG1OVGMxTVdGaE9XSXlOemxqWTJVeU0yRmhNVGM0WVdabU9UVTJaVEU1WmpZNU1HWmpaREptT1dZM01HVXdaRGs1T1RjNE1tRTJPVEJrTm1JNElpd2ljMk5vWlcxaFgzWmxjbk5wYjI0aU9pSnZjR1Z1WVdSaGNIUXVjSEp2WkhWamRHbHZiaTFrWld4cGRtVnllUzF3WlhKdGFYUXRZWEowYVdaaFkzUXZkaklpTENKemFXZHVZWFIxY21VaU9pSXdRbmxJWjNGRWVGRmFjbTVuVEVsR0xUWjBXazl4T1d4WVlXMWpRVkZETUdSdlZWUnVaVXRPVUdWVExWTjViMDlpVVhsUGEwRjFVRVpMTlZCWlkwNTZUMkphZUdkelowRk1aRmRoU1Y5ellWVkRjR3hDWnlJc0luTnBaMjVsY2lJNmV5SmhiR2R2Y21sMGFHMGlPaUpsWkRJMU5URTVJaXdpYTJWNVgybGtJam9pWkdWc2FYWmxjbmt0WVhWMGFHOXlhWFI1TFdWa01qVTFNVGt0TWpSbU5tVmtObUZqWW1abE1UQXdPU0lzSW5CMVlteHBZMTlyWlhraU9pSkxZWGsyTkZWSE9IbDJRM2xNYUhGVk1EQXdUSGg2V1dWVmJUQk1MMmhNU1d3MVV6aHJlVXRYWW1SalBTSjlmU3dpY0dWeWJXbDBYMkZ5ZEdsbVlXTjBYM05vWVRJMU5pSTZJamszTW1FNE1tRm1ORFUyWkdFMU5ESmlZV00zT0RRMk1ERTRPRGsyTmpnNE9URmtPR0kzWTJOa01qUXlPVEEwT0RFMllqTXlPVEpsTVRVM1pUVXlPRGtpTENKeVpXTmxhWEIwWDJGaWMyVnVZMlZmYjJKelpYSjJaV1JmWVhRaU9pSXlNREkyTFRBNExURTRWREV5T2pBd09qQXlXaUlzSW5OamFHVnRZVjkyWlhKemFXOXVJam9pYjNCbGJtRmtZWEIwTG5CeWIyUjFZM1JwYjI0dFpHVnNhWFpsY25rdGNHVnVaR2x1Wnkxd1pYSnRhWFF2ZGpJaWZTd2ljR1Z5YldsMFgyTm9ZV2x1WDNOb1lUSTFOaUk2SWpVNVlqTTVPVGN4WlRNd05URXdPV00xTkRJNU9UUTFZVFZtWVdJME5qWXdZakE1WmpReFpUZGhZek5tWm1RNU1tVXpOVEEyWVdZd01ETXpOMlJoT1RVaUxDSnpZMmhsYldGZmRtVnljMmx2YmlJNkltOXdaVzVoWkdGd2RDNXdjbTlrZFdOMGFXOXVMV1JsYkdsMlpYSjVMWEJsY20xcGRDMWphR0ZwYmk5Mk1pSjkiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTliMzk5NzFlMzA1MTA5YzU0Mjk5NDVhNWZhYjQ2NjBiMDlmNDFlN2FjM2ZmZDkyZTM1MDZhZjAwMzM3ZGE5NSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1yZXN1bHQvdjIiLCJzdGF0dXMiOiJjbG9zZWQifQ==" }, "private_key_base64": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "public_key_base64": "A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=", @@ -158,22 +158,22 @@ "uncertain_delivery": true }, "effect_state": null, - "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlIjp7InBheWxvYWQiOnsiYWNrbm93bGVkZ2VkX3Blcm1pdF9jb3VudCI6MCwiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6IjY1YTZiZWQ5MjNlNDVhNmFhMGM2MjQzMDQ1ZTAzNjRjOTBhY2UwNjQ4ZmVjYjViMGJiMmViZDk0MDI3OTNjNzciLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJjbG9zZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsImNsb3N1cmVfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTEiLCJjbG9zdXJlX3JlcXVlc3Rfc2hhMjU2IjoiYmFmNDE3YWU0OGVlYzQwNjU4NGVmNTI4NmFhYTFiMWFjMmU2ZTAzZDhhMjlhNGZiNGIwYzUzMjcwNGJlNTljNiIsImNsb3N1cmVfc2VxdWVuY2UiOjEsImRlbGl2ZXJ5X2Fja25vd2xlZGdtZW50X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9pbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjowLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbXBvdGVuY3lfa2V5X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJuZXdfcGVybWl0X2F1dGhvcml6ZWQiOmZhbHNlLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsInBlbmRpbmdfYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsInBlbmRpbmdfcGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJwZXJtaXRfY291bnQiOjEsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicmVzdWx0X2xvc3Nfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW5fc3RvcmVfaWRlbnRpdHlfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1wYXlsb2FkL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfY2FsbGJhY2tfcmVxdWlyZWQiOnRydWV9LCJwYXlsb2FkX3NoYTI1NiI6IjhkYjU4ZjZhMDVmNGVmMWY4Mjc3NjYwMGQ1ZTJhMTMwNjQ1YTNlMTJiNTBmZWU2ODEwODg3NGVmZjA4ZjdlYmYiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJhakNhd3h3WTFZaUFVMXdwM2t1cFlHa3pubDcyVkFUcGxIV2dZMXVYNTdqRU9Gal80czJfdlhQbFVFQXlVbXBOaHA0eG9xVFhCUDhjOE9lMkpMZTNEdyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV2aWRlbmNlX2lkZW50aXR5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJldmlkZW5jZV9tYW5pZmVzdHMiOnsiYXV0aG9yaXphdGlvbiI6eyJhZG1pc3Npb25fYXJ0aWZhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNiIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJnb3Zlcm5lZF9hdXRob3JpemF0aW9uX2lkX3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJtYW5pZmVzdF9zaGEyNTYiOiIxNWMwZTc2ZGMyZTMyMWEzODRlNTNlYzE1ZmY1M2U3NTU1N2I3Y2RlNzFjNWFkMmJhMmI5M2ZmMmU5NTgzNWE1IiwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWF1dGhvcml6YXRpb24tZXZpZGVuY2UvdjEifSwiZWZmZWN0Ijp7ImNvbmZpcm1lZCI6MCwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJtYW5pZmVzdF9zaGEyNTYiOiI3ZTEyZGNhNWM0NGNmOTk4YTQ2OTM2MmI4NjJlODE2YTdkMmM4NTQxNjNmNjNlOGRlNjFmYjc5ZTk1YmE4MDA3IiwicmVjb3JkcyI6W10sInJlcXVpcmVkIjowLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWVmZmVjdC1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiaWRlbnRpdHkiOnsiY29uZmlybWVkIjowLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiZDNhN2VhYzM0MjdkNzZmNmJjMzYwMzA3Mzg2MDkxMTlmMDdlMGQ5MWE2ZDM1OGFiY2U1YjZmMGQ2ZGY3ODY0NSIsInJlcXVpcmVkIjowLCJyZXN1bHRzIjpbXSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1pZGVudGl0eS1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwicG9saWN5Ijp7ImFkbWlzc2lvbl9wb2xpY3lfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImVmZmVjdF9jb250cmFjdF9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZW52aXJvbm1lbnRfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJnb3Zlcm5lZF9wb2xpY3lfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImdvdmVybmVkX3J1bnRpbWVfaW5wdXRzX2RpZ2VzdCI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiNGY5ODhkMDE1YmNkN2NkN2UwNWJhMDY2M2Q4OWYwYTk2OGYwZDhhY2U5OWIwOTBlYmFiZWU1MmExYzU1MDQyMSIsIm1pbmltdW1fZWZmZWN0X3RpZXIiOjEsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9saWN5LWV2aWRlbmNlL3YxIn0sInBvc3Rjb25kaXRpb24iOnsiY29uZmlybWVkIjowLCJtYW5pZmVzdF9zaGEyNTYiOiI1MmYwOWQ1ZDYzOWIxYzM4M2YzNzc5NjNlNDg3Y2I4OWQ4OTQxNzg1N2U0NGRhN2UzZjIzZTU1YmU3NTFlN2Y0IiwicmVjb3JkcyI6W10sInJlcXVpcmVkIjowLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvc3Rjb25kaXRpb24tZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn19LCJldmlkZW5jZV9ydW5uZXJfc2lnbmVyX3NoYTI1NiI6IjU2NDc1YWE3NTQ2MzQ3NGMwMjg1ZGY1ZGJmMmJjYWI3M2RhNjUxMzU4ODM5ZTliNzc0ODFiMmVhYjEwNzcwOGMiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImV4ZWN1dGlvbl9vdXRjb21lIjp7ImNvbXBlbnNhdGlvbl9hY3Rpb25zIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iXSwiZXhlY3V0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJub25lIiwibWFuYWdlZF9yZXN1bHRfbG9zc19ldmlkZW5jZV9zaGEyNTYiOiIzODEzMTljMWQ3NjJhMjNjMjExNmEyZWUwNzQxNzZjMDZmOWM4NDIwYjk1MTM4ZWFkYjliNjdkYTZhM2I0NDQ3IiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJwYXNzZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjAsInBvc3Rjb25kaXRpb24iOjB9LCJwb3N0Y29uZGl0aW9uX2V2aWRlbmNlIjpbXSwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInF1YWxpZmljYXRpb25fZXZpZGVuY2Vfb25seSI6ZmFsc2UsInJlcXVpcmVkX2NvbnRyYWN0cyI6eyJhdXRob3JpemF0aW9uIjoxLCJlZmZlY3QiOjAsImlkZW50aXR5IjowLCJwb3N0Y29uZGl0aW9uIjowfSwidmVyc2lvbiI6Im9wZW5hZGFwdC5leGVjdXRpb24tb3V0Y29tZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiZXhlY3V0aW9uX291dGNvbWVfc2hhMjU2IjoiNzBjZjZmODE3NzJiMGJlNGIxZDcwNjdhMDVhYjRjMjkzMzY5MjM2ZDAyOWFhMWJlOGQ3OWQ2YzdjNzMxM2VmNSIsImV4ZWN1dGlvbl9wdXJwb3NlIjoicHJvZHVjdGlvbiIsImZpbmFsX2F1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZmluYWxfcnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6MCwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAzWiIsIm1hbmFnZWRfcmVzdWx0X2xvc3MiOnsiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3JlcG9ydF9yZXRhaW5lZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6IjY1YTZiZWQ5MjNlNDVhNmFhMGM2MjQzMDQ1ZTAzNjRjOTBhY2UwNjQ4ZmVjYjViMGJiMmViZDk0MDI3OTNjNzciLCJjaGlsZF9zdGFydGVkIjp0cnVlLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlX2FydGlmYWN0X3NoYTI1NiI6ImNlYzM1N2FjZGI3NGY0ZTAyZGQxYzMyMDlhOGVjODgwOTE1Yzk4MzAxMzZlMDcyNjBmM2MxZjYzMWY4ZDUyZTgiLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV2aWRlbmNlX3NoYTI1NiI6IjM4MTMxOWMxZDc2MmEyM2MyMTE2YTJlZTA3NDE3NmMwNmY5Yzg0MjBiOTUxMzhlYWRiOWI2N2RhNmEzYjQ0NDciLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpZGVtcG90ZW5jeV9rZXlfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImxvc3NfY29kZSI6InJlcG9ydF9taXNzaW5nIiwibWFuYWdlZF9kaXNwYXRjaF9iaW5kaW5nX3NoYTI1NiI6InNoYTI1NjpjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwibm90X3JlY2VpdmVkX2NsYWltZWQiOmZhbHNlLCJvYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicGVuZGluZ19hY3Rpb25fcmVxdWVzdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwicGVuZGluZ19wZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInJlcG9ydF9wcm92ZW5hbmNlIjoib3V0ZXJfcnVubmVyX3JlY292ZXJ5IiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bl9zdG9yZV9pZGVudGl0eV9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQubWFuYWdlZC1yZXN1bHQtbG9zcy92MSJ9LCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MSwicGVybWl0X2NoYWluIjp7ImVudHJpZXMiOltdLCJwZW5kaW5nIjp7ImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3QiOm51bGwsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3Rfc2hhMjU2IjpudWxsLCJkZWxpdmVyeV9zdGF0ZSI6IlVOUkVTT0xWRUQiLCJwZXJtaXRfYXJ0aWZhY3QiOnsicGF5bG9hZCI6eyJhY3Rpb25fcmVxdWVzdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2FydGlmYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJhdXRob3JpdHlfc2VxdWVuY2UiOjAsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJldmlkZW5jZV9pZGVudGl0eV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsInBlcm1pdF9pZCI6InBlcm1pdDoxIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfY2hlY2tlZF9hdCI6IjIwMjYtMDgtMThUMTE6NTk6MzBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfZXhwaXJlc19hdCI6IjIwMjYtMDgtMjBUMTE6MDA6MDBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6ImM4N2Y1NzUxYWE5YjI3OWNjZTIzYWExNzhhZmY5NTZlMTlmNjkwZmNkMmY5ZjcwZTBkOTk5NzgyYTY5MGQ2YjgiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6IjBCeUhncUR4UVpybmdMSUYtNnRaT3E5bFhhbWNBUUMwZG9VVG5lS05QZVMtU3lvT2JReU9rQXVQRks1UFljTnpPYlp4Z3NnQUxkV2FJX3NhVUNwbEJnIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJwZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZW5kaW5nLXBlcm1pdC92MiJ9LCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTliMzk5NzFlMzA1MTA5YzU0Mjk5NDVhNWZhYjQ2NjBiMDlmNDFlN2FjM2ZmZDkyZTM1MDZhZjAwMzM3ZGE5NSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWNoYWluL3YyIn0sInBlcm1pdF9jb3VudCI6MSwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVjZWlwdCI6eyJhdXRob3JpemF0aW9uX2NvbmZpcm1lZCI6MSwiYXV0aG9yaXphdGlvbl9yZXF1aXJlZCI6MSwiYnVuZGxlX2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJkdXJhdGlvbl9tcyI6MCwiZWZmZWN0X3RpZXJfcmVhY2hlZCI6Im5vbmUiLCJlZmZlY3RzX2NvbmZpcm1lZCI6MCwiZWZmZWN0c19yZXF1aXJlZCI6MCwiZXN0X2Nvc3RfbWljcm91c2QiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiJdLCJleHRlcm5hbF9uZXR3b3JrX2NhbGxzIjoibm9uZSIsImdlbmVyYXRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiaGVhbHMiOjAsImlkZW50aXR5X2FwcGxpY2FibGUiOjAsImlkZW50aXR5X2FybWVkIjowLCJpZGVudGl0eV9jb25maXJtZWQiOjAsImlkZW50aXR5X3JlcXVpcmVkIjowLCJtYW5hZ2VkX3Jlc3VsdF9sb3NzX2V2aWRlbmNlX3NoYTI1NiI6IjM4MTMxOWMxZDc2MmEyM2MyMTE2YTJlZTA3NDE3NmMwNmY5Yzg0MjBiOTUxMzhlYWRiOWI2N2RhNmEzYjQ0NDciLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsIm92ZXJfaGFsdF9jb3VudCI6MCwicG9zdGNvbmRpdGlvbnNfY29uZmlybWVkIjowLCJwb3N0Y29uZGl0aW9uc19yZXF1aXJlZCI6MCwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInByb3ZlbmFuY2UiOiJwcm9kdWN0aW9uIiwicmVjZWlwdF9idWlsZGVyX3ZlcnNpb24iOiIxLjIuMyIsInJ1bmdfaGlzdG9ncmFtIjp7fSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1ydW4tcmVjZWlwdC92MSIsInNvdXJjZV9yZWNlaXB0X2RpZ2VzdCI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzb3VyY2VfcmVjZWlwdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3NjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnJ1bi1yZXBvcnQvdjEiLCJzdGVwc19vayI6MCwic3RlcHNfdG90YWwiOjEsInN1YnN0cmF0ZSI6IndlYiIsInRyYW5zYWN0aW9uX291dGNvbWUiOiJSRUNPTkNJTElBVElPTl9SRVFVSVJFRCJ9LCJydW5fcmVjZWlwdF9zaGEyNTYiOiJhN2RhZmIxNWFiODE4MzhmNGIzNGRhZDA4MTI2OTJhN2E1ZjE5ZTAwNzkzNWUwZDc2NTIwNDFiOGU3MDUwYWUyIiwicnVuX3JlcG9ydF9vYmplY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bl9yZXBvcnRfb2JqZWN0X3ZlcnNpb24iOiJ2ZXJzaW9uOm1hbmFnZWQtcmVzdWx0LWxvc3M6MSIsInJ1bl9yZXBvcnRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bnRpbWVfc3Vic3RyYXRlIjoid2ViIiwicnVudGltZV92YWxpZGF0aW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA1Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi10ZXJtaW5hbC12ZXJpZmljYXRpb24vdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9zZXF1ZW5jZSI6MSwidmVyaWZpZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJ3b3JrZmxvd19pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMyIsIndvcmtmbG93X3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQifSwic2lnbmF0dXJlIjoiVzRUMmpQelZ3azFoM2NuR1JoVkZ5aTgwalZyanZkandRR04tTVpyeFQ2R2pVejljbDV4UXloajd2LURabkxFTkFzcGlnLTBGeEV1WVJHeU9tWkdDQVEiLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImV2aWRlbmNlLXJ1bm5lci1lZDI1NTE5LTU2NDc1YWE3NTQ2MzQ3NGMiLCJwdWJsaWNfa2V5IjoiQTZFSHYvUE9FTDRkY04wWTUwdkFtV2ZrMWpDYnBRMWZIZHlHWkJKVk1iZz0ifX0=", + "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlIjp7InBheWxvYWQiOnsiYWNrbm93bGVkZ2VkX3Blcm1pdF9jb3VudCI6MCwiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6ImVjNjkwNzkxODg2YWUyY2UwMGM0ZTUxMTU0YzNmMTIwZWE0NDE5ZWQyNjUxZTA3M2RiNTcyNDkxOGEzYjA4YzYiLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJjbG9zZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsImNsb3N1cmVfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTEiLCJjbG9zdXJlX3JlcXVlc3Rfc2hhMjU2IjoiZDkzMGFiMWFiYzJiMmEwNGI4NDhiOTU1ZmZkZmZmYTFmYmUwNjU1YTU1MzI1ZTA0ODlhZTExZWQ4NDIwMjI2MCIsImNsb3N1cmVfc2VxdWVuY2UiOjEsImRlbGl2ZXJ5X2Fja25vd2xlZGdtZW50X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9pbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjowLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbXBvdGVuY3lfa2V5X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJuZXdfcGVybWl0X2F1dGhvcml6ZWQiOmZhbHNlLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsInBlbmRpbmdfYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsInBlbmRpbmdfcGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJwZXJtaXRfY291bnQiOjEsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicmVzdWx0X2xvc3Nfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW5fc3RvcmVfaWRlbnRpdHlfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1wYXlsb2FkL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfY2FsbGJhY2tfcmVxdWlyZWQiOnRydWV9LCJwYXlsb2FkX3NoYTI1NiI6ImE4OTFhYzRjYmE1ZmRmYzE3ZDAzYzFmODAyYTJkZjhjMTFmYmYwYjYyZTRmMDRiZTY3ZjFhNWM2ZTY4ZDBhNTMiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJnYnN0M241STMwQUF1dzZqMTdSREUtQWVRZXlxc0prUnVNbHJHQjdKSUNPOFNuTHhlQkhaSzM0NVRiN1ljY2lVZjVITF9COFZOdEpaRFRSd0ZpM1VEUSIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV2aWRlbmNlX2lkZW50aXR5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJldmlkZW5jZV9tYW5pZmVzdHMiOnsiYXV0aG9yaXphdGlvbiI6eyJhZG1pc3Npb25fYXJ0aWZhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNiIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJnb3Zlcm5lZF9hdXRob3JpemF0aW9uX2lkX3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJtYW5pZmVzdF9zaGEyNTYiOiIxNWMwZTc2ZGMyZTMyMWEzODRlNTNlYzE1ZmY1M2U3NTU1N2I3Y2RlNzFjNWFkMmJhMmI5M2ZmMmU5NTgzNWE1IiwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWF1dGhvcml6YXRpb24tZXZpZGVuY2UvdjEifSwiZWZmZWN0Ijp7ImNvbmZpcm1lZCI6MCwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJtYW5pZmVzdF9zaGEyNTYiOiI3ZTEyZGNhNWM0NGNmOTk4YTQ2OTM2MmI4NjJlODE2YTdkMmM4NTQxNjNmNjNlOGRlNjFmYjc5ZTk1YmE4MDA3IiwicmVjb3JkcyI6W10sInJlcXVpcmVkIjowLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWVmZmVjdC1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiaWRlbnRpdHkiOnsiY29uZmlybWVkIjowLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiZDNhN2VhYzM0MjdkNzZmNmJjMzYwMzA3Mzg2MDkxMTlmMDdlMGQ5MWE2ZDM1OGFiY2U1YjZmMGQ2ZGY3ODY0NSIsInJlcXVpcmVkIjowLCJyZXN1bHRzIjpbXSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1pZGVudGl0eS1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwicG9saWN5Ijp7ImFkbWlzc2lvbl9wb2xpY3lfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImVmZmVjdF9jb250cmFjdF9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZW52aXJvbm1lbnRfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJnb3Zlcm5lZF9wb2xpY3lfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImdvdmVybmVkX3J1bnRpbWVfaW5wdXRzX2RpZ2VzdCI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiNGY5ODhkMDE1YmNkN2NkN2UwNWJhMDY2M2Q4OWYwYTk2OGYwZDhhY2U5OWIwOTBlYmFiZWU1MmExYzU1MDQyMSIsIm1pbmltdW1fZWZmZWN0X3RpZXIiOjEsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9saWN5LWV2aWRlbmNlL3YxIn0sInBvc3Rjb25kaXRpb24iOnsiY29uZmlybWVkIjowLCJtYW5pZmVzdF9zaGEyNTYiOiI1MmYwOWQ1ZDYzOWIxYzM4M2YzNzc5NjNlNDg3Y2I4OWQ4OTQxNzg1N2U0NGRhN2UzZjIzZTU1YmU3NTFlN2Y0IiwicmVjb3JkcyI6W10sInJlcXVpcmVkIjowLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvc3Rjb25kaXRpb24tZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn19LCJldmlkZW5jZV9ydW5uZXJfc2lnbmVyX3NoYTI1NiI6IjU2NDc1YWE3NTQ2MzQ3NGMwMjg1ZGY1ZGJmMmJjYWI3M2RhNjUxMzU4ODM5ZTliNzc0ODFiMmVhYjEwNzcwOGMiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImV4ZWN1dGlvbl9vdXRjb21lIjp7ImNvbXBlbnNhdGlvbl9hY3Rpb25zIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iXSwiZXhlY3V0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJub25lIiwibWFuYWdlZF9yZXN1bHRfbG9zc19ldmlkZW5jZV9zaGEyNTYiOiIyY2QwNzY0OTBjYTVlNWU4ZDlkMGJkMTQ2ZDU1OWMwNmVkNDEzYTY3ZDY5NDgxYTFhZmNjMzU0NDY0ZWNkYjA3IiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJwYXNzZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjAsInBvc3Rjb25kaXRpb24iOjB9LCJwb3N0Y29uZGl0aW9uX2V2aWRlbmNlIjpbXSwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInF1YWxpZmljYXRpb25fZXZpZGVuY2Vfb25seSI6ZmFsc2UsInJlcXVpcmVkX2NvbnRyYWN0cyI6eyJhdXRob3JpemF0aW9uIjoxLCJlZmZlY3QiOjAsImlkZW50aXR5IjowLCJwb3N0Y29uZGl0aW9uIjowfSwidmVyc2lvbiI6Im9wZW5hZGFwdC5leGVjdXRpb24tb3V0Y29tZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiZXhlY3V0aW9uX291dGNvbWVfc2hhMjU2IjoiMzQ0OTYyZDJkZTM2ZWNiZDI5NWIxZTViZjhhZmIwNmE3ZjQ2NWQ2NTRlODFjZGZiNTIxNmZiYjYxMWYxZGE4YiIsImV4ZWN1dGlvbl9wdXJwb3NlIjoicHJvZHVjdGlvbiIsImZpbmFsX2F1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZmluYWxfcnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6MCwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAzWiIsIm1hbmFnZWRfcmVzdWx0X2xvc3MiOnsiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3JlcG9ydF9yZXRhaW5lZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6ImVjNjkwNzkxODg2YWUyY2UwMGM0ZTUxMTU0YzNmMTIwZWE0NDE5ZWQyNjUxZTA3M2RiNTcyNDkxOGEzYjA4YzYiLCJjaGlsZF9zdGFydGVkIjp0cnVlLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlX2FydGlmYWN0X3NoYTI1NiI6IjY1ZDBkODJkNTc3YTUzMjMwMjJiMmFmY2UxYTI2YTIxNmE1MmQyMWVmOTgyZTA1NDBjMjY2ZjMwNjNmYmM3OTciLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV2aWRlbmNlX3NoYTI1NiI6IjJjZDA3NjQ5MGNhNWU1ZThkOWQwYmQxNDZkNTU5YzA2ZWQ0MTNhNjdkNjk0ODFhMWFmY2MzNTQ0NjRlY2RiMDciLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpZGVtcG90ZW5jeV9rZXlfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImxvc3NfY29kZSI6InJlcG9ydF9taXNzaW5nIiwibWFuYWdlZF9kaXNwYXRjaF9iaW5kaW5nX3NoYTI1NiI6InNoYTI1NjpjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwibm90X3JlY2VpdmVkX2NsYWltZWQiOmZhbHNlLCJvYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicGVuZGluZ19hY3Rpb25fcmVxdWVzdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwicGVuZGluZ19wZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInJlcG9ydF9wcm92ZW5hbmNlIjoib3V0ZXJfcnVubmVyX3JlY292ZXJ5IiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bl9zdG9yZV9pZGVudGl0eV9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQubWFuYWdlZC1yZXN1bHQtbG9zcy92MSJ9LCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MSwicGVybWl0X2NoYWluIjp7ImVudHJpZXMiOltdLCJwZW5kaW5nIjp7ImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3QiOm51bGwsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3Rfc2hhMjU2IjpudWxsLCJkZWxpdmVyeV9zdGF0ZSI6IlVOUkVTT0xWRUQiLCJwZXJtaXRfYXJ0aWZhY3QiOnsicGF5bG9hZCI6eyJhY3Rpb25fcmVxdWVzdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2FydGlmYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJhdXRob3JpdHlfc2VxdWVuY2UiOjAsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJldmlkZW5jZV9pZGVudGl0eV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsInBlcm1pdF9pZCI6InBlcm1pdDoxIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfY2hlY2tlZF9hdCI6IjIwMjYtMDgtMThUMTE6NTk6MzBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfZXhwaXJlc19hdCI6IjIwMjYtMDgtMjBUMTE6MDA6MDBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6ImM4N2Y1NzUxYWE5YjI3OWNjZTIzYWExNzhhZmY5NTZlMTlmNjkwZmNkMmY5ZjcwZTBkOTk5NzgyYTY5MGQ2YjgiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6IjBCeUhncUR4UVpybmdMSUYtNnRaT3E5bFhhbWNBUUMwZG9VVG5lS05QZVMtU3lvT2JReU9rQXVQRks1UFljTnpPYlp4Z3NnQUxkV2FJX3NhVUNwbEJnIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJwZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZW5kaW5nLXBlcm1pdC92MiJ9LCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTliMzk5NzFlMzA1MTA5YzU0Mjk5NDVhNWZhYjQ2NjBiMDlmNDFlN2FjM2ZmZDkyZTM1MDZhZjAwMzM3ZGE5NSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWNoYWluL3YyIn0sInBlcm1pdF9jb3VudCI6MSwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVjZWlwdCI6eyJhdXRob3JpemF0aW9uX2NvbmZpcm1lZCI6MSwiYXV0aG9yaXphdGlvbl9yZXF1aXJlZCI6MSwiYnVuZGxlX2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJkdXJhdGlvbl9tcyI6MCwiZWZmZWN0X3RpZXJfcmVhY2hlZCI6Im5vbmUiLCJlZmZlY3RzX2NvbmZpcm1lZCI6MCwiZWZmZWN0c19yZXF1aXJlZCI6MCwiZXN0X2Nvc3RfbWljcm91c2QiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiJdLCJleHRlcm5hbF9uZXR3b3JrX2NhbGxzIjoibm9uZSIsImdlbmVyYXRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiaGVhbHMiOjAsImlkZW50aXR5X2FwcGxpY2FibGUiOjAsImlkZW50aXR5X2FybWVkIjowLCJpZGVudGl0eV9jb25maXJtZWQiOjAsImlkZW50aXR5X3JlcXVpcmVkIjowLCJtYW5hZ2VkX3Jlc3VsdF9sb3NzX2V2aWRlbmNlX3NoYTI1NiI6IjJjZDA3NjQ5MGNhNWU1ZThkOWQwYmQxNDZkNTU5YzA2ZWQ0MTNhNjdkNjk0ODFhMWFmY2MzNTQ0NjRlY2RiMDciLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsIm92ZXJfaGFsdF9jb3VudCI6MCwicG9zdGNvbmRpdGlvbnNfY29uZmlybWVkIjowLCJwb3N0Y29uZGl0aW9uc19yZXF1aXJlZCI6MCwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInByb3ZlbmFuY2UiOiJwcm9kdWN0aW9uIiwicmVjZWlwdF9idWlsZGVyX3ZlcnNpb24iOiIxLjIuMyIsInJ1bmdfaGlzdG9ncmFtIjp7fSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1ydW4tcmVjZWlwdC92MSIsInNvdXJjZV9yZWNlaXB0X2RpZ2VzdCI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzb3VyY2VfcmVjZWlwdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3NjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnJ1bi1yZXBvcnQvdjEiLCJzdGVwc19vayI6MCwic3RlcHNfdG90YWwiOjEsInN1YnN0cmF0ZSI6IndlYiIsInRyYW5zYWN0aW9uX291dGNvbWUiOiJSRUNPTkNJTElBVElPTl9SRVFVSVJFRCJ9LCJydW5fcmVjZWlwdF9zaGEyNTYiOiIwOTU1YjBlNzllMDlmYjFlMzRiZGI5OGE4ZDEzMGE4YzEyNjNiZTg2Y2E5ZDU0ODYyZDdkMzRhYmZmNGNkOWI0IiwicnVuX3JlcG9ydF9vYmplY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bl9yZXBvcnRfb2JqZWN0X3ZlcnNpb24iOiJ2ZXJzaW9uOm1hbmFnZWQtcmVzdWx0LWxvc3M6MSIsInJ1bl9yZXBvcnRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bnRpbWVfc3Vic3RyYXRlIjoid2ViIiwicnVudGltZV92YWxpZGF0aW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA1Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi10ZXJtaW5hbC12ZXJpZmljYXRpb24vdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9zZXF1ZW5jZSI6MSwidmVyaWZpZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJ3b3JrZmxvd19pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMyIsIndvcmtmbG93X3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQifSwic2lnbmF0dXJlIjoidUd0S1hWY2pxS0tyTEhPMm44dGhmYUdsbDk5bHpFeHF0MlcyUHpSZlQyOG9udXpzNjk5VGxtMDVkSzlCVTlCZC16WDM3NEJDcjBtVktIeHJ2WnRsQlEiLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImV2aWRlbmNlLXJ1bm5lci1lZDI1NTE5LTU2NDc1YWE3NTQ2MzQ3NGMiLCJwdWJsaWNfa2V5IjoiQTZFSHYvUE9FTDRkY04wWTUwdkFtV2ZrMWpDYnBRMWZIZHlHWkJKVk1iZz0ifX0=", "managed_result_loss": { "actuation_replay_authorized": false, - "authenticated_runner_id_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "authenticated_session_id_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authenticated_runner_id_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authenticated_session_id_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "blind_retry_authorized": false, "child_report_retained": false, - "child_start_evidence_sha256": "65a6bed923e45a6aa0c6243045e0364c90ace0648fecb5b0bb2ebd9402793c77", + "child_start_evidence_sha256": "ec690791886ae2ce00c4e51154c3f120ea4419ed2651e073db5724918a3b08c6", "child_started": true, "child_started_at": "2026-08-18T12:00:00Z", - "delivery_result_loss_closure_artifact_sha256": "cec357acdb74f4e02dd1c3209a8ec880915c9830136e07260f3c1f631f8d52e8", + "delivery_result_loss_closure_artifact_sha256": "65d0d82d577a5323022b2afce1a26a216a52d21ef982e0540c266f3063fbc797", "delivery_state": "CLOSED_UNRESOLVED_RESULT_LOSS", "dispatch_id": "00000000-0000-4000-8000-000000000009", "dispatch_session_id": "00000000-0000-4000-8000-000000000010", "effect_absence_claimed": false, - "evidence_sha256": "381319c1d762a23c2116a2ee074176c06f9c8420b95138eadb9b67da6a3b4447", + "evidence_sha256": "2cd076490ca5e5e8d9d0bd146d559c06ed413a67d69481a1afcc354464ecdb07", "execution_authority_id": "00000000-0000-4000-8000-000000000008", "execution_authority_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "execution_authority_signer_sha256": "24f6ed6acbfe1009c030d7ca567c33ca4830911498236b5561a6c82abec5de28", @@ -192,9 +192,9 @@ "schema_version": "openadapt.managed-result-loss/v1" }, "name": "reconciliation-required-managed-result-loss", - "payload_canonical_sha256": "1480dcd4830987d01d27a66647e8b38af02a605d93ca604c5cbc50c10a7a1852", - "signature": "W4T2jPzVwk1h3cnGRhVFyi80jVrjvdjwQGN-MZrxT6GjUz9cl5xQyhj7v-DZnLENAspig-0FxEuYRGyOmZGCAQ", - "terminal_verification_artifact_sha256": "fa3eb5109494d421cefff1557211b19614fbf4be654c188832c8130c3308899e" + "payload_canonical_sha256": "955bae00518f0d07965a05727654e108b618818fd6933f6de2ed8c3d425f27fd", + "signature": "uGtKXVcjqKKrLHO2n8thfaGll99lzExqt2W2PzRfT28onuzs699Tlm05dK9BU9Bd-zX374BCr0mVKHxrvZtlBQ", + "terminal_verification_artifact_sha256": "c2a7ef78aebeb55c585386b7b78e16ac3c84f8d19656d0dd1cb76b65141b8d47" }, { "callback": { @@ -207,22 +207,22 @@ "uncertain_delivery": false }, "effect_state": null, - "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjEsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlIjp7InBheWxvYWQiOnsiYWNrbm93bGVkZ2VkX3Blcm1pdF9jb3VudCI6MSwiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6IjY1YTZiZWQ5MjNlNDVhNmFhMGM2MjQzMDQ1ZTAzNjRjOTBhY2UwNjQ4ZmVjYjViMGJiMmViZDk0MDI3OTNjNzciLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJjbG9zZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsImNsb3N1cmVfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTEiLCJjbG9zdXJlX3JlcXVlc3Rfc2hhMjU2IjoiYmFmNDE3YWU0OGVlYzQwNjU4NGVmNTI4NmFhYTFiMWFjMmU2ZTAzZDhhMjlhNGZiNGIwYzUzMjcwNGJlNTljNiIsImNsb3N1cmVfc2VxdWVuY2UiOjEsImRlbGl2ZXJ5X2Fja25vd2xlZGdtZW50X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9pbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbXBvdGVuY3lfa2V5X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJuZXdfcGVybWl0X2F1dGhvcml6ZWQiOmZhbHNlLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsInBlbmRpbmdfYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjpudWxsLCJwZW5kaW5nX3Blcm1pdF9hcnRpZmFjdF9zaGEyNTYiOm51bGwsInBlbmRpbmdfcGVybWl0X2NvdW50IjowLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInBlcm1pdF9jb3VudCI6MSwicmVjZWlwdF9hYnNlbmNlX29ic2VydmVkX2F0IjpudWxsLCJyZXN1bHRfbG9zc19vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bl9zdG9yZV9pZGVudGl0eV9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZXN1bHQtbG9zcy1jbG9zdXJlLXBheWxvYWQvdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9jYWxsYmFja19yZXF1aXJlZCI6dHJ1ZX0sInBheWxvYWRfc2hhMjU2IjoiMTE5YjBkODA2Njk0ZWQyMTYwNGQwOTE3MTMyMmQ1MjFiYzBhYTEyYjFlYTc5ZmIzNDkyNTA0YWMzZWZjNjRlYSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6InZsUmhUdHFSdm9kYnNObWZ5MmFSU3pndU0tYmZkZDRUWkFyeHBvbGRHd0NSeWtDZWRVLUZhanhkdHlyTDd5TmNpQnZpdWNvTnBpUFUyQXBNdG9abEJnIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6ImYwY2ExOWE2YjNlYWJlNTk0ZGJjOTA2ODljMmY3NGNiZTVlODJkNGQ5NDdkYjI5YTVkMDdiMTY0MWU4OTYxY2YiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6IjdlMTJkY2E1YzQ0Y2Y5OThhNDY5MzYyYjg2MmU4MTZhN2QyYzg1NDE2M2Y2M2U4ZGU2MWZiNzllOTViYTgwMDciLCJyZWNvcmRzIjpbXSwicmVxdWlyZWQiOjAsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZWZmZWN0LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJpZGVudGl0eSI6eyJjb25maXJtZWQiOjAsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiJkM2E3ZWFjMzQyN2Q3NmY2YmMzNjAzMDczODYwOTExOWYwN2UwZDkxYTZkMzU4YWJjZTViNmYwZDZkZjc4NjQ1IiwicmVxdWlyZWQiOjAsInJlc3VsdHMiOltdLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWlkZW50aXR5LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJwb2xpY3kiOnsiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX3BvbGljeV9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZ292ZXJuZWRfcnVudGltZV9pbnB1dHNfZGlnZXN0IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiI0Zjk4OGQwMTViY2Q3Y2Q3ZTA1YmEwNjYzZDg5ZjBhOTY4ZjBkOGFjZTk5YjA5MGViYWJlZTUyYTFjNTUwNDIxIiwibWluaW11bV9lZmZlY3RfdGllciI6MSwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb2xpY3ktZXZpZGVuY2UvdjEifSwicG9zdGNvbmRpdGlvbiI6eyJjb25maXJtZWQiOjAsIm1hbmlmZXN0X3NoYTI1NiI6IjUyZjA5ZDVkNjM5YjFjMzgzZjM3Nzk2M2U0ODdjYjg5ZDg5NDE3ODU3ZTQ0ZGE3ZTNmMjNlNTViZTc1MWU3ZjQiLCJyZWNvcmRzIjpbXSwicmVxdWlyZWQiOjAsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9zdGNvbmRpdGlvbi1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifX0sImV2aWRlbmNlX3J1bm5lcl9zaWduZXJfc2hhMjU2IjoiNTY0NzVhYTc1NDYzNDc0YzAyODVkZjVkYmYyYmNhYjczZGE2NTEzNTg4MzllOWI3NzQ4MWIyZWFiMTA3NzA4YyIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZXhlY3V0aW9uX291dGNvbWUiOnsiY29tcGVuc2F0aW9uX2FjdGlvbnMiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiJdLCJleGVjdXRpb25fY29tcGxldGVkIjpmYWxzZSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJtYW5hZ2VkX3Jlc3VsdF9sb3NzX2V2aWRlbmNlX3NoYTI1NiI6IjcxNDUyYjExODgzYWJjZDYzZDhjNzYxMzNkNDI2MDEyMjhmYWExYWZlY2Q4Mjc5YjVlY2NlODY2MThkNGViMjMiLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsInBhc3NlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjowLCJpZGVudGl0eSI6MCwicG9zdGNvbmRpdGlvbiI6MH0sInBvc3Rjb25kaXRpb25fZXZpZGVuY2UiOltdLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjpmYWxzZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicXVhbGlmaWNhdGlvbl9ldmlkZW5jZV9vbmx5IjpmYWxzZSwicmVxdWlyZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjAsInBvc3Rjb25kaXRpb24iOjB9LCJ2ZXJzaW9uIjoib3BlbmFkYXB0LmV4ZWN1dGlvbi1vdXRjb21lL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJleGVjdXRpb25fb3V0Y29tZV9zaGEyNTYiOiI0ODlhYmU5MzQ0OGNjYzc5NWZhZWY0NGJjZTcwMTE1MzQwODM2ZDVjNDc1ZDIwZGIzZmNmOWY2NTY2NTJmYzRkIiwiZXhlY3V0aW9uX3B1cnBvc2UiOiJwcm9kdWN0aW9uIiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDNaIiwibWFuYWdlZF9yZXN1bHRfbG9zcyI6eyJhY3R1YXRpb25fcmVwbGF5X2F1dGhvcml6ZWQiOmZhbHNlLCJhdXRoZW50aWNhdGVkX3J1bm5lcl9pZF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYXV0aGVudGljYXRlZF9zZXNzaW9uX2lkX3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJibGluZF9yZXRyeV9hdXRob3JpemVkIjpmYWxzZSwiY2hpbGRfcmVwb3J0X3JldGFpbmVkIjpmYWxzZSwiY2hpbGRfc3RhcnRfZXZpZGVuY2Vfc2hhMjU2IjoiNjVhNmJlZDkyM2U0NWE2YWEwYzYyNDMwNDVlMDM2NGM5MGFjZTA2NDhmZWNiNWIwYmIyZWJkOTQwMjc5M2M3NyIsImNoaWxkX3N0YXJ0ZWQiOnRydWUsImNoaWxkX3N0YXJ0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImRlbGl2ZXJ5X3Jlc3VsdF9sb3NzX2Nsb3N1cmVfYXJ0aWZhY3Rfc2hhMjU2IjoiOTBkZjhiZDFmMjg5MWIyMDM2NjNhNjNmNDI1MTUwNzQ0NWRiZTgzODM2MGEyMTllNDNhZjlhMWJjZTBhODBhMSIsImRlbGl2ZXJ5X3N0YXRlIjoiQ0xPU0VEX1VOUkVTT0xWRURfUkVTVUxUX0xPU1MiLCJkaXNwYXRjaF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOSIsImRpc3BhdGNoX3Nlc3Npb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTAiLCJlZmZlY3RfYWJzZW5jZV9jbGFpbWVkIjpmYWxzZSwiZXZpZGVuY2Vfc2hhMjU2IjoiNzE0NTJiMTE4ODNhYmNkNjNkOGM3NjEzM2Q0MjYwMTIyOGZhYTFhZmVjZDgyNzliNWVjY2U4NjYxOGQ0ZWIyMyIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW1wb3RlbmN5X2tleV9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibG9zc19jb2RlIjoicmVwb3J0X21pc3NpbmciLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsIm9ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJwZW5kaW5nX2FjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6bnVsbCwicGVuZGluZ19wZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjpudWxsLCJyZXBvcnRfcHJvdmVuYW5jZSI6Im91dGVyX3J1bm5lcl9yZWNvdmVyeSIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW5fc3RvcmVfaWRlbnRpdHlfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0Lm1hbmFnZWQtcmVzdWx0LWxvc3MvdjEifSwicGVuZGluZ19wZXJtaXRfY291bnQiOjAsInBlcm1pdF9jaGFpbiI6eyJlbnRyaWVzIjpbeyJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiZGVsaXZlcmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMVoiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4Iiwib25lX3VzZV9jbGFpbV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsInBlcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5IiwicGVybWl0X2lkIjoicGVybWl0OjEiLCJydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6IjhhOGY2ZDg4NzUwY2JiNmIzYmQ2NjIwOTNhZDI5ZTJiYzIwYzZhYjY5OTFkNzBlNTg4NjkxYzhiM2FiNjYyYjkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJBT0txQ2dTMXMwWGEtaXlFOTZWLXlsUHpRY3YydXZPc3V5Z0h6dnUyUnlWN0ZMc0hGX0xWZnF5eDR1MnB1bWxFbDBZazVZdTcwSlZVa3NacGFVcEREdyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwiZGVsaXZlcnlfcmVjZWlwdF9hcnRpZmFjdF9zaGEyNTYiOiJjMDBhMWJhNzAxNWYzY2JlMDVhNzZhZWFiZmY2MTlkNzkzZTczNzVhMzEwZTE5NWZjYmJkMmQyOTQ4ZjJiMjE0IiwicGVybWl0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJwZXJtaXRfaWQiOiJwZXJtaXQ6MSIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2NoZWNrZWRfYXQiOiIyMDI2LTA4LTE4VDExOjU5OjMwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2V4cGlyZXNfYXQiOiIyMDI2LTA4LTIwVDExOjAwOjAwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiJjODdmNTc1MWFhOWIyNzljY2UyM2FhMTc4YWZmOTU2ZTE5ZjY5MGZjZDJmOWY3MGUwZDk5OTc4MmE2OTBkNmI4Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiIwQnlIZ3FEeFFacm5nTElGLTZ0Wk9xOWxYYW1jQVFDMGRvVVRuZUtOUGVTLVN5b09iUXlPa0F1UEZLNVBZY056T2JaeGdzZ0FMZFdhSV9zYVVDcGxCZyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwicGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi1lbnRyeS92MiJ9XSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjUwNDhmZTk0NTg4MmQyODAyNjAwOGQ3ZTkxMzdhZDAxNmZkZDAxYWVhZjU4ODQ4MWZiNWJkMGYyNDJhNTAwNDMiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi92MiJ9LCJwZXJtaXRfY291bnQiOjEsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlY2VpcHQiOnsiYXV0aG9yaXphdGlvbl9jb25maXJtZWQiOjEsImF1dGhvcml6YXRpb25fcmVxdWlyZWQiOjEsImJ1bmRsZV9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZHVyYXRpb25fbXMiOjAsImVmZmVjdF90aWVyX3JlYWNoZWQiOiJub25lIiwiZWZmZWN0c19jb25maXJtZWQiOjAsImVmZmVjdHNfcmVxdWlyZWQiOjAsImVzdF9jb3N0X21pY3JvdXNkIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iXSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJnZW5lcmF0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImhlYWxzIjowLCJpZGVudGl0eV9hcHBsaWNhYmxlIjowLCJpZGVudGl0eV9hcm1lZCI6MCwiaWRlbnRpdHlfY29uZmlybWVkIjowLCJpZGVudGl0eV9yZXF1aXJlZCI6MCwibWFuYWdlZF9yZXN1bHRfbG9zc19ldmlkZW5jZV9zaGEyNTYiOiI3MTQ1MmIxMTg4M2FiY2Q2M2Q4Yzc2MTMzZDQyNjAxMjI4ZmFhMWFmZWNkODI3OWI1ZWNjZTg2NjE4ZDRlYjIzIiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJvdmVyX2hhbHRfY291bnQiOjAsInBvc3Rjb25kaXRpb25zX2NvbmZpcm1lZCI6MCwicG9zdGNvbmRpdGlvbnNfcmVxdWlyZWQiOjAsInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJwcm92ZW5hbmNlIjoicHJvZHVjdGlvbiIsInJlY2VpcHRfYnVpbGRlcl92ZXJzaW9uIjoiMS4yLjMiLCJydW5nX2hpc3RvZ3JhbSI6e30sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcnVuLXJlY2VpcHQvdjEiLCJzb3VyY2VfcmVjZWlwdF9kaWdlc3QiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3JlY2VpcHRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9zY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5ydW4tcmVwb3J0L3YxIiwic3RlcHNfb2siOjAsInN0ZXBzX3RvdGFsIjoxLCJzdWJzdHJhdGUiOiJ3ZWIiLCJ0cmFuc2FjdGlvbl9vdXRjb21lIjoiUkVDT05DSUxJQVRJT05fUkVRVUlSRUQifSwicnVuX3JlY2VpcHRfc2hhMjU2IjoiNDhiZDFkNmRjOTVkYmNhOTlhMTM4ZDE2ZjIzMmNkNjkxMDI1ZThhNGUxM2E0OGFlYTA4ZTI4NmRkMjA2NzlhNSIsInJ1bl9yZXBvcnRfb2JqZWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW5fcmVwb3J0X29iamVjdF92ZXJzaW9uIjoidmVyc2lvbjptYW5hZ2VkLXJlc3VsdC1sb3NzOjEiLCJydW5fcmVwb3J0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW50aW1lX3N1YnN0cmF0ZSI6IndlYiIsInJ1bnRpbWVfdmFsaWRhdGlvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfc2VxdWVuY2UiOjEsInZlcmlmaWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwid29ya2Zsb3dfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJ3b3JrZmxvd192ZXJzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA0In0sInNpZ25hdHVyZSI6ImM2OFVFYUdXejVfb2tRMDN2cGZjWGJOS1poWEE1Y2FfV3dpaTBPeGdhVGI3RzJXc1FFZGVjZGI0Z0tqVDIzck5FRGtBRTFaQzRBb1ZpTUdXcFRlaERRIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJldmlkZW5jZS1ydW5uZXItZWQyNTUxOS01NjQ3NWFhNzU0NjM0NzRjIiwicHVibGljX2tleSI6IkE2RUh2L1BPRUw0ZGNOMFk1MHZBbVdmazFqQ2JwUTFmSGR5R1pCSlZNYmc9In19", + "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjEsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlIjp7InBheWxvYWQiOnsiYWNrbm93bGVkZ2VkX3Blcm1pdF9jb3VudCI6MSwiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6ImVjNjkwNzkxODg2YWUyY2UwMGM0ZTUxMTU0YzNmMTIwZWE0NDE5ZWQyNjUxZTA3M2RiNTcyNDkxOGEzYjA4YzYiLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJjbG9zZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsImNsb3N1cmVfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTEiLCJjbG9zdXJlX3JlcXVlc3Rfc2hhMjU2IjoiZDkzMGFiMWFiYzJiMmEwNGI4NDhiOTU1ZmZkZmZmYTFmYmUwNjU1YTU1MzI1ZTA0ODlhZTExZWQ4NDIwMjI2MCIsImNsb3N1cmVfc2VxdWVuY2UiOjEsImRlbGl2ZXJ5X2Fja25vd2xlZGdtZW50X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9pbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbXBvdGVuY3lfa2V5X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJuZXdfcGVybWl0X2F1dGhvcml6ZWQiOmZhbHNlLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsInBlbmRpbmdfYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjpudWxsLCJwZW5kaW5nX3Blcm1pdF9hcnRpZmFjdF9zaGEyNTYiOm51bGwsInBlbmRpbmdfcGVybWl0X2NvdW50IjowLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInBlcm1pdF9jb3VudCI6MSwicmVjZWlwdF9hYnNlbmNlX29ic2VydmVkX2F0IjpudWxsLCJyZXN1bHRfbG9zc19vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bl9zdG9yZV9pZGVudGl0eV9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZXN1bHQtbG9zcy1jbG9zdXJlLXBheWxvYWQvdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9jYWxsYmFja19yZXF1aXJlZCI6dHJ1ZX0sInBheWxvYWRfc2hhMjU2IjoiNmI3NWY4YjQ1YTdkM2U5Y2ZlODU2NGZmZTMyZjI5YzJiNTk3YzI4MTM2YTk3YTRmNTQ1M2ZlMmFmZDE3MjhhYSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6Imt6dVIycXJjR1Q0cG82LXl6ZFlEZ2gzd2FZamI5V3BRR1VfQm5RTEhUSWJkZDV4QmxyVVNsNkg0Y2VWQzMzcjFCd2JGdFJQYVB2bzA1cGd4Z0U0d0F3Iiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6ImYwY2ExOWE2YjNlYWJlNTk0ZGJjOTA2ODljMmY3NGNiZTVlODJkNGQ5NDdkYjI5YTVkMDdiMTY0MWU4OTYxY2YiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6IjdlMTJkY2E1YzQ0Y2Y5OThhNDY5MzYyYjg2MmU4MTZhN2QyYzg1NDE2M2Y2M2U4ZGU2MWZiNzllOTViYTgwMDciLCJyZWNvcmRzIjpbXSwicmVxdWlyZWQiOjAsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZWZmZWN0LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJpZGVudGl0eSI6eyJjb25maXJtZWQiOjAsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiJkM2E3ZWFjMzQyN2Q3NmY2YmMzNjAzMDczODYwOTExOWYwN2UwZDkxYTZkMzU4YWJjZTViNmYwZDZkZjc4NjQ1IiwicmVxdWlyZWQiOjAsInJlc3VsdHMiOltdLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWlkZW50aXR5LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJwb2xpY3kiOnsiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX3BvbGljeV9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZ292ZXJuZWRfcnVudGltZV9pbnB1dHNfZGlnZXN0IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiI0Zjk4OGQwMTViY2Q3Y2Q3ZTA1YmEwNjYzZDg5ZjBhOTY4ZjBkOGFjZTk5YjA5MGViYWJlZTUyYTFjNTUwNDIxIiwibWluaW11bV9lZmZlY3RfdGllciI6MSwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb2xpY3ktZXZpZGVuY2UvdjEifSwicG9zdGNvbmRpdGlvbiI6eyJjb25maXJtZWQiOjAsIm1hbmlmZXN0X3NoYTI1NiI6IjUyZjA5ZDVkNjM5YjFjMzgzZjM3Nzk2M2U0ODdjYjg5ZDg5NDE3ODU3ZTQ0ZGE3ZTNmMjNlNTViZTc1MWU3ZjQiLCJyZWNvcmRzIjpbXSwicmVxdWlyZWQiOjAsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9zdGNvbmRpdGlvbi1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifX0sImV2aWRlbmNlX3J1bm5lcl9zaWduZXJfc2hhMjU2IjoiNTY0NzVhYTc1NDYzNDc0YzAyODVkZjVkYmYyYmNhYjczZGE2NTEzNTg4MzllOWI3NzQ4MWIyZWFiMTA3NzA4YyIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZXhlY3V0aW9uX291dGNvbWUiOnsiY29tcGVuc2F0aW9uX2FjdGlvbnMiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiJdLCJleGVjdXRpb25fY29tcGxldGVkIjpmYWxzZSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJtYW5hZ2VkX3Jlc3VsdF9sb3NzX2V2aWRlbmNlX3NoYTI1NiI6ImYyMmYyNzA0NGVmMzE1ZTkzOWFiYWRmNmI2NzI1ZmNmMmI5MmYyYTI4MDM5NDA3NTgwZTBmMmY3ZTIzMDU0OTQiLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsInBhc3NlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjowLCJpZGVudGl0eSI6MCwicG9zdGNvbmRpdGlvbiI6MH0sInBvc3Rjb25kaXRpb25fZXZpZGVuY2UiOltdLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjpmYWxzZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicXVhbGlmaWNhdGlvbl9ldmlkZW5jZV9vbmx5IjpmYWxzZSwicmVxdWlyZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjAsInBvc3Rjb25kaXRpb24iOjB9LCJ2ZXJzaW9uIjoib3BlbmFkYXB0LmV4ZWN1dGlvbi1vdXRjb21lL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJleGVjdXRpb25fb3V0Y29tZV9zaGEyNTYiOiJhOTUxNjFlYjFmOGZjMzBmYjk0ZGNiNzdjZDIzYzc2MjdlM2NhY2EzODBhZmEzNGU0MTE3YzI2NGNmYzhiMmYzIiwiZXhlY3V0aW9uX3B1cnBvc2UiOiJwcm9kdWN0aW9uIiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDNaIiwibWFuYWdlZF9yZXN1bHRfbG9zcyI6eyJhY3R1YXRpb25fcmVwbGF5X2F1dGhvcml6ZWQiOmZhbHNlLCJhdXRoZW50aWNhdGVkX3J1bm5lcl9pZF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiYXV0aGVudGljYXRlZF9zZXNzaW9uX2lkX3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJibGluZF9yZXRyeV9hdXRob3JpemVkIjpmYWxzZSwiY2hpbGRfcmVwb3J0X3JldGFpbmVkIjpmYWxzZSwiY2hpbGRfc3RhcnRfZXZpZGVuY2Vfc2hhMjU2IjoiZWM2OTA3OTE4ODZhZTJjZTAwYzRlNTExNTRjM2YxMjBlYTQ0MTllZDI2NTFlMDczZGI1NzI0OTE4YTNiMDhjNiIsImNoaWxkX3N0YXJ0ZWQiOnRydWUsImNoaWxkX3N0YXJ0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImRlbGl2ZXJ5X3Jlc3VsdF9sb3NzX2Nsb3N1cmVfYXJ0aWZhY3Rfc2hhMjU2IjoiYjJkOTJmMzI0OGY0ZGE2MWVkYjAyN2NmNTlkYTUzOTVmZmM0MThlYWQ2ZDQ4NWMyOWRmNmMzNWJjMjFhOTMwZCIsImRlbGl2ZXJ5X3N0YXRlIjoiQ0xPU0VEX1VOUkVTT0xWRURfUkVTVUxUX0xPU1MiLCJkaXNwYXRjaF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOSIsImRpc3BhdGNoX3Nlc3Npb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTAiLCJlZmZlY3RfYWJzZW5jZV9jbGFpbWVkIjpmYWxzZSwiZXZpZGVuY2Vfc2hhMjU2IjoiZjIyZjI3MDQ0ZWYzMTVlOTM5YWJhZGY2YjY3MjVmY2YyYjkyZjJhMjgwMzk0MDc1ODBlMGYyZjdlMjMwNTQ5NCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW1wb3RlbmN5X2tleV9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibG9zc19jb2RlIjoicmVwb3J0X21pc3NpbmciLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsIm9ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJwZW5kaW5nX2FjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6bnVsbCwicGVuZGluZ19wZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjpudWxsLCJyZXBvcnRfcHJvdmVuYW5jZSI6Im91dGVyX3J1bm5lcl9yZWNvdmVyeSIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW5fc3RvcmVfaWRlbnRpdHlfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0Lm1hbmFnZWQtcmVzdWx0LWxvc3MvdjEifSwicGVuZGluZ19wZXJtaXRfY291bnQiOjAsInBlcm1pdF9jaGFpbiI6eyJlbnRyaWVzIjpbeyJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiZGVsaXZlcmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMVoiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4Iiwib25lX3VzZV9jbGFpbV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsInBlcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5IiwicGVybWl0X2lkIjoicGVybWl0OjEiLCJydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6IjhhOGY2ZDg4NzUwY2JiNmIzYmQ2NjIwOTNhZDI5ZTJiYzIwYzZhYjY5OTFkNzBlNTg4NjkxYzhiM2FiNjYyYjkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJBT0txQ2dTMXMwWGEtaXlFOTZWLXlsUHpRY3YydXZPc3V5Z0h6dnUyUnlWN0ZMc0hGX0xWZnF5eDR1MnB1bWxFbDBZazVZdTcwSlZVa3NacGFVcEREdyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwiZGVsaXZlcnlfcmVjZWlwdF9hcnRpZmFjdF9zaGEyNTYiOiJjMDBhMWJhNzAxNWYzY2JlMDVhNzZhZWFiZmY2MTlkNzkzZTczNzVhMzEwZTE5NWZjYmJkMmQyOTQ4ZjJiMjE0IiwicGVybWl0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJwZXJtaXRfaWQiOiJwZXJtaXQ6MSIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2NoZWNrZWRfYXQiOiIyMDI2LTA4LTE4VDExOjU5OjMwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2V4cGlyZXNfYXQiOiIyMDI2LTA4LTIwVDExOjAwOjAwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiJjODdmNTc1MWFhOWIyNzljY2UyM2FhMTc4YWZmOTU2ZTE5ZjY5MGZjZDJmOWY3MGUwZDk5OTc4MmE2OTBkNmI4Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiIwQnlIZ3FEeFFacm5nTElGLTZ0Wk9xOWxYYW1jQVFDMGRvVVRuZUtOUGVTLVN5b09iUXlPa0F1UEZLNVBZY056T2JaeGdzZ0FMZFdhSV9zYVVDcGxCZyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwicGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi1lbnRyeS92MiJ9XSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjUwNDhmZTk0NTg4MmQyODAyNjAwOGQ3ZTkxMzdhZDAxNmZkZDAxYWVhZjU4ODQ4MWZiNWJkMGYyNDJhNTAwNDMiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi92MiJ9LCJwZXJtaXRfY291bnQiOjEsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlY2VpcHQiOnsiYXV0aG9yaXphdGlvbl9jb25maXJtZWQiOjEsImF1dGhvcml6YXRpb25fcmVxdWlyZWQiOjEsImJ1bmRsZV9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZHVyYXRpb25fbXMiOjAsImVmZmVjdF90aWVyX3JlYWNoZWQiOiJub25lIiwiZWZmZWN0c19jb25maXJtZWQiOjAsImVmZmVjdHNfcmVxdWlyZWQiOjAsImVzdF9jb3N0X21pY3JvdXNkIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iXSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJnZW5lcmF0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImhlYWxzIjowLCJpZGVudGl0eV9hcHBsaWNhYmxlIjowLCJpZGVudGl0eV9hcm1lZCI6MCwiaWRlbnRpdHlfY29uZmlybWVkIjowLCJpZGVudGl0eV9yZXF1aXJlZCI6MCwibWFuYWdlZF9yZXN1bHRfbG9zc19ldmlkZW5jZV9zaGEyNTYiOiJmMjJmMjcwNDRlZjMxNWU5MzlhYmFkZjZiNjcyNWZjZjJiOTJmMmEyODAzOTQwNzU4MGUwZjJmN2UyMzA1NDk0IiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJvdmVyX2hhbHRfY291bnQiOjAsInBvc3Rjb25kaXRpb25zX2NvbmZpcm1lZCI6MCwicG9zdGNvbmRpdGlvbnNfcmVxdWlyZWQiOjAsInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJwcm92ZW5hbmNlIjoicHJvZHVjdGlvbiIsInJlY2VpcHRfYnVpbGRlcl92ZXJzaW9uIjoiMS4yLjMiLCJydW5nX2hpc3RvZ3JhbSI6e30sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcnVuLXJlY2VpcHQvdjEiLCJzb3VyY2VfcmVjZWlwdF9kaWdlc3QiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3JlY2VpcHRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9zY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5ydW4tcmVwb3J0L3YxIiwic3RlcHNfb2siOjAsInN0ZXBzX3RvdGFsIjoxLCJzdWJzdHJhdGUiOiJ3ZWIiLCJ0cmFuc2FjdGlvbl9vdXRjb21lIjoiUkVDT05DSUxJQVRJT05fUkVRVUlSRUQifSwicnVuX3JlY2VpcHRfc2hhMjU2IjoiMzI5NzEzZDkwZTA1YjBmNTVlNmFhNTM2NWJmZThiMzA2NGY1ZTQ3OTBlMWNkYmRjOThlMzYyM2Y4N2NlYzVjYyIsInJ1bl9yZXBvcnRfb2JqZWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW5fcmVwb3J0X29iamVjdF92ZXJzaW9uIjoidmVyc2lvbjptYW5hZ2VkLXJlc3VsdC1sb3NzOjEiLCJydW5fcmVwb3J0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW50aW1lX3N1YnN0cmF0ZSI6IndlYiIsInJ1bnRpbWVfdmFsaWRhdGlvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfc2VxdWVuY2UiOjEsInZlcmlmaWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwid29ya2Zsb3dfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJ3b3JrZmxvd192ZXJzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA0In0sInNpZ25hdHVyZSI6IlgwcWtfTkJXVFVKVk1LQ2NFaFJHcWNJeDRjOTdYd1U2Szg4T0plbHdxc2FpWGVtTV9nRlJ4MHlNYjdPcjhfZUlSNmpMNDNDSVNPMEc0Ums3SWVYNUN3Iiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJldmlkZW5jZS1ydW5uZXItZWQyNTUxOS01NjQ3NWFhNzU0NjM0NzRjIiwicHVibGljX2tleSI6IkE2RUh2L1BPRUw0ZGNOMFk1MHZBbVdmazFqQ2JwUTFmSGR5R1pCSlZNYmc9In19", "managed_result_loss": { "actuation_replay_authorized": false, - "authenticated_runner_id_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "authenticated_session_id_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authenticated_runner_id_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authenticated_session_id_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "blind_retry_authorized": false, "child_report_retained": false, - "child_start_evidence_sha256": "65a6bed923e45a6aa0c6243045e0364c90ace0648fecb5b0bb2ebd9402793c77", + "child_start_evidence_sha256": "ec690791886ae2ce00c4e51154c3f120ea4419ed2651e073db5724918a3b08c6", "child_started": true, "child_started_at": "2026-08-18T12:00:00Z", - "delivery_result_loss_closure_artifact_sha256": "90df8bd1f2891b203663a63f4251507445dbe838360a219e43af9a1bce0a80a1", + "delivery_result_loss_closure_artifact_sha256": "b2d92f3248f4da61edb027cf59da5395ffc418ead6d485c29df6c35bc21a930d", "delivery_state": "CLOSED_UNRESOLVED_RESULT_LOSS", "dispatch_id": "00000000-0000-4000-8000-000000000009", "dispatch_session_id": "00000000-0000-4000-8000-000000000010", "effect_absence_claimed": false, - "evidence_sha256": "71452b11883abcd63d8c76133d42601228faa1afecd8279b5ecce86618d4eb23", + "evidence_sha256": "f22f27044ef315e939abadf6b6725fcf2b92f2a28039407580e0f2f7e2305494", "execution_authority_id": "00000000-0000-4000-8000-000000000008", "execution_authority_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "execution_authority_signer_sha256": "24f6ed6acbfe1009c030d7ca567c33ca4830911498236b5561a6c82abec5de28", @@ -241,9 +241,9 @@ "schema_version": "openadapt.managed-result-loss/v1" }, "name": "reconciliation-required-managed-result-loss-acknowledged", - "payload_canonical_sha256": "f636736f6796ba6246fdd2d6c0668642928ad5990342e801b882b2c22013eae3", - "signature": "c68UEaGWz5_okQ03vpfcXbNKZhXA5ca_Wwii0OxgaTb7G2WsQEdecdb4gKjT23rNEDkAE1ZC4AoViMGWpTehDQ", - "terminal_verification_artifact_sha256": "b78ff44e81d4be51fbb28132c871aab660616a8ec5eba0dcde24e6298c0cd87f" + "payload_canonical_sha256": "c024765711724b2645dbe23c250d301079e9045cc45f54eba72bacab3a22233b", + "signature": "X0qk_NBWTUJVMKCcEhRGqcIx4c97XwU6K88OJelwqsaiXemM_gFRx0yMb7Or8_eIR6jL43CISO0G4Rk7IeX5Cw", + "terminal_verification_artifact_sha256": "1707d4b13e8549cb7b12edae186866006b0211933830384d9fca6e1fc0b54756" } ] } diff --git a/tests/test_terminal_verification_v2.py b/tests/test_terminal_verification_v2.py index ed8195ff..92cc246f 100644 --- a/tests/test_terminal_verification_v2.py +++ b/tests/test_terminal_verification_v2.py @@ -69,6 +69,7 @@ sign_production_delivery_receipt, sign_production_delivery_result_loss_closure, sign_production_terminal_verification, + verify_production_delivery_result_loss_closure_binding, verify_production_terminal_verification, ) from tests.test_run_receipt import _report as _production_report @@ -806,8 +807,8 @@ def _result_loss_request() -> ProductionDeliveryResultLossClosureRequest: dispatch_session_id="00000000-0000-4000-8000-000000000010", run_id=IDS["run_id"], managed_dispatch_binding_sha256="sha256:" + SHA_C, - authenticated_runner_id_sha256=SHA_A, - authenticated_session_id_sha256=SHA_B, + authenticated_runner_id_sha256=SHA_B, + authenticated_session_id_sha256=SHA_C, execution_authority_id=IDS["execution_authority_id"], execution_authority_sha256=SHA_A, execution_authority_signer_sha256=(_permit_entry().authority_signer_sha256), @@ -847,8 +848,12 @@ def _result_loss_closure( dispatch_session_id="00000000-0000-4000-8000-000000000010", managed_dispatch_binding_sha256="sha256:" + SHA_C, idempotency_key_sha256=SHA_D, - authenticated_runner_id_sha256=SHA_A, - authenticated_session_id_sha256=SHA_B, + authenticated_runner_id_sha256=( + request.child_start_evidence.authenticated_runner_id_sha256 + ), + authenticated_session_id_sha256=( + request.child_start_evidence.authenticated_session_id_sha256 + ), execution_authority_id=IDS["execution_authority_id"], execution_authority_sha256=SHA_A, execution_authority_signer_sha256=final.authority_signer_sha256, @@ -900,8 +905,12 @@ def _managed_result_loss_payload( dispatch_session_id="00000000-0000-4000-8000-000000000010", managed_dispatch_binding_sha256="sha256:" + SHA_C, idempotency_key_sha256=SHA_D, - authenticated_runner_id_sha256=SHA_A, - authenticated_session_id_sha256=SHA_B, + authenticated_runner_id_sha256=( + request.child_start_evidence.authenticated_runner_id_sha256 + ), + authenticated_session_id_sha256=( + request.child_start_evidence.authenticated_session_id_sha256 + ), execution_authority_id=IDS["execution_authority_id"], execution_authority_sha256=SHA_A, execution_authority_signer_sha256=final.authority_signer_sha256, @@ -1330,6 +1339,44 @@ def test_terminal_v2_signs_ack_won_managed_result_loss_without_uncertainty() -> assert payload.evidence_manifests.effect.records == () +@pytest.mark.parametrize( + "identity_field", + ["authenticated_runner_id_sha256", "authenticated_session_id_sha256"], +) +def test_ack_won_result_loss_closure_rejects_receipt_identity_mismatch( + identity_field: str, +) -> None: + payload = _managed_result_loss_acknowledged_payload() + closure = payload.delivery_result_loss_closure + loss = payload.managed_result_loss + assert closure is not None + assert loss is not None + mismatched_payload = closure.payload.model_copy(update={identity_field: SHA_E}) + mismatched_closure = sign_production_delivery_result_loss_closure( + mismatched_payload, + _authority_key(), + ) + loss_values = loss.model_dump(mode="json") + loss_values.pop("evidence_sha256") + loss_values[identity_field] = SHA_E + loss_values["delivery_result_loss_closure_artifact_sha256"] = ( + mismatched_closure.artifact_sha256() + ) + mismatched_loss = ManagedResultLossEvidence.create(**loss_values) + + with pytest.raises( + ValueError, + match="authenticated delivery identity", + ): + verify_production_delivery_result_loss_closure_binding( + mismatched_closure, + permit_chain=payload.permit_chain, + result_loss=mismatched_loss, + tenant_id=payload.tenant_id, + terminal_verified_at=payload.verified_at, + ) + + @pytest.mark.parametrize( "field", [ From 9ada2eab8043bdc02b12932d0db55d96b445b5c5 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Thu, 27 Aug 2026 16:05:32 -0400 Subject: [PATCH 9/9] fix(hosted): fence every managed result loss --- openadapt_flow/ir.py | 1 + openadapt_flow/runner/hosted_adapter.py | 173 ++++++++++++---- openadapt_flow/terminal_verification_v2.py | 2 + public-artifacts.json | 2 +- ...nerate_terminal_verification_v2_vectors.py | 84 ++++---- ...inal_verification_v2_terminal_vectors.json | 31 ++- tests/test_hosted_runner_adapter.py | 193 +++++++++++++++++- tests/test_terminal_verification_v2.py | 105 ++++++---- 8 files changed, 464 insertions(+), 127 deletions(-) diff --git a/openadapt_flow/ir.py b/openadapt_flow/ir.py index 6f337eb1..d5b6d126 100644 --- a/openadapt_flow/ir.py +++ b/openadapt_flow/ir.py @@ -3405,6 +3405,7 @@ class ManagedResultLossEvidence(BaseModel): loss_code: Literal[ "runner_exception", "report_missing", + "report_invalid", "recovered_after_restart", ] child_started_at: str = Field( diff --git a/openadapt_flow/runner/hosted_adapter.py b/openadapt_flow/runner/hosted_adapter.py index 107b70b6..9a0b7e42 100644 --- a/openadapt_flow/runner/hosted_adapter.py +++ b/openadapt_flow/runner/hosted_adapter.py @@ -115,6 +115,12 @@ _RUN_STORE_IDENTITY_DOMAIN = b"OpenAdapt managed run store identity v1\0" _RESULT_LOSS_SNAPSHOT_DOMAIN = b"OpenAdapt managed result loss snapshot v1\0" _MAX_TERMINAL_STATE_BYTES = 2 * (4 * ((_MAX_ARTIFACT_BYTES + 2) // 3)) + 4096 +ManagedResultLossCode = Literal[ + "runner_exception", + "report_missing", + "report_invalid", + "recovered_after_restart", +] # The current runner credential is never part of a request model. Desktop may # project it into this header only for POST /api/runners/register on the exact @@ -1489,29 +1495,17 @@ def _managed_result_loss_report( raise ValueError("managed result loss classification changed") return RunReport.model_validate_json(report.model_dump_json()) - def _produce_managed_result_loss( + def _retain_managed_result_loss_closure( self, *, dispatch: HostedDispatch, - workflow: Workflow, run_dir: Path, - qualification: ProductionQualificationAuthority, - private_key: Ed25519PrivateKey, - verified_params: dict[str, RuntimeParamScalar], dispatch_binding_sha256: str, closure_authority: HostedRunnerTransport, - loss_code: Literal[ - "runner_exception", - "report_missing", - "recovered_after_restart", - ], - ) -> tuple[ProductionTerminalVerificationEnvelope, str, RunReport]: - """Seal one result-loss report without resolving or replaying delivery.""" + loss_code: ManagedResultLossCode, + ) -> ManagedResultLossSnapshot: + """Fence delivery authority before any fallible terminal proof work.""" - store = CheckpointStore(run_dir) - manifest = store.read_manifest() - if manifest is None: - raise ValueError("managed result loss lacks a retained run manifest") marker = self._read_child_start_evidence( dispatch=dispatch, run_dir=run_dir, @@ -1521,11 +1515,7 @@ def _produce_managed_result_loss( def snapshot_from_closure( *, request: ProductionDeliveryResultLossClosureRequest, - retained_loss_code: Literal[ - "runner_exception", - "report_missing", - "recovered_after_restart", - ], + retained_loss_code: ManagedResultLossCode, closure_artifact: ProductionDeliveryResultLossClosureArtifact, retained_chain: ProductionDeliveryPermitChain, ) -> ManagedResultLossSnapshot: @@ -1696,6 +1686,43 @@ def read_retained_snapshot() -> ManagedResultLossSnapshot: snapshot = read_retained_snapshot() else: snapshot = read_retained_snapshot() + return snapshot + + def _produce_managed_result_loss( + self, + *, + dispatch: HostedDispatch, + workflow: Workflow, + run_dir: Path, + qualification: ProductionQualificationAuthority, + private_key: Ed25519PrivateKey, + verified_params: dict[str, RuntimeParamScalar], + dispatch_binding_sha256: str, + loss_code: ManagedResultLossCode, + closure_authority: HostedRunnerTransport | None = None, + snapshot: ManagedResultLossSnapshot | None = None, + ) -> tuple[ProductionTerminalVerificationEnvelope, str, RunReport]: + """Sign one result-loss terminal after Cloud has fenced delivery.""" + + if snapshot is None: + if closure_authority is None: + raise ValueError("hosted result-loss closure authority is absent") + snapshot = self._retain_managed_result_loss_closure( + dispatch=dispatch, + run_dir=run_dir, + dispatch_binding_sha256=dispatch_binding_sha256, + closure_authority=closure_authority, + loss_code=loss_code, + ) + store = CheckpointStore(run_dir) + manifest = store.read_manifest() + if manifest is None: + raise ValueError("managed result loss lacks a retained run manifest") + marker = self._read_child_start_evidence( + dispatch=dispatch, + run_dir=run_dir, + dispatch_binding_sha256=dispatch_binding_sha256, + ) evidence = snapshot.evidence observed_at = evidence.observed_at expected = qualification.expected @@ -2432,16 +2459,6 @@ def recover_uncertain_run( """ parsed = HostedDispatch.model_validate(dispatch) - config = load_runner_config(runner_config, protected=True) - self._protected_runner_origin(config) - self._verify_product_release(parsed, config) - key = self._load_evidence_private_key(config) - qualification, deployment_bytes = self._verify_workflow_admission( - parsed, - config, - evidence_private_key=key, - ) - params = self._resolve_params(parsed, config) run_dir = Path(run_dir) run_stat = run_dir.lstat() if ( @@ -2456,6 +2473,33 @@ def recover_uncertain_run( ) ): raise ValueError("hosted recovery run directory is not protected") + self._read_child_start_evidence( + dispatch=parsed, + run_dir=run_dir, + dispatch_binding_sha256=parsed.payload.dispatch_binding_sha256, + ) + reservation_key = f"{parsed.tenant_id}:{parsed.idempotency_key}" + reservation = self._ledger.lookup(reservation_key) + if reservation is None or reservation.get("run_id") != parsed.run_id: + raise ValueError("hosted recovery lacks the original reservation") + loss_snapshot = self._retain_managed_result_loss_closure( + dispatch=parsed, + run_dir=run_dir, + dispatch_binding_sha256=parsed.payload.dispatch_binding_sha256, + closure_authority=closure_authority, + loss_code="recovered_after_restart", + ) + + config = load_runner_config(runner_config, protected=True) + self._protected_runner_origin(config) + self._verify_product_release(parsed, config) + key = self._load_evidence_private_key(config) + qualification, deployment_bytes = self._verify_workflow_admission( + parsed, + config, + evidence_private_key=key, + ) + params = self._resolve_params(parsed, config) profile_path = run_dir / "deployment.yaml" if ( self._read_private_bytes( @@ -2475,15 +2519,10 @@ def recover_uncertain_run( ) if isinstance(verified, Refusal): raise ValueError("hosted recovery dispatch no longer verifies") - self._read_child_start_evidence( - dispatch=parsed, - run_dir=run_dir, - dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, - ) - reservation_key = f"{parsed.tenant_id}:{parsed.idempotency_key}" - reservation = self._ledger.lookup(reservation_key) - if reservation is None or reservation.get("run_id") != parsed.run_id: - raise ValueError("hosted recovery lacks the original reservation") + if verified.payload.dispatch_binding_sha256 != ( + parsed.payload.dispatch_binding_sha256 + ): + raise ValueError("hosted recovery dispatch binding changed") proof, report_digest, report = self._produce_managed_result_loss( dispatch=parsed, @@ -2493,8 +2532,8 @@ def recover_uncertain_run( private_key=key, verified_params=params, dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, - closure_authority=closure_authority, loss_code="recovered_after_restart", + snapshot=loss_snapshot, ) outcome = TransactionOutcome.RECONCILIATION_REQUIRED self._ledger.record_outcome(reservation_key, outcome, run_id=parsed.run_id) @@ -2706,6 +2745,13 @@ def execute( try: if closure_authority is None: raise ValueError("hosted result-loss closure authority is absent") + loss_snapshot = self._retain_managed_result_loss_closure( + dispatch=parsed, + run_dir=run_dir, + dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, + closure_authority=closure_authority, + loss_code="runner_exception", + ) terminal_key, terminal_qualification = ( self._revalidate_terminal_authority( dispatch=parsed, @@ -2724,8 +2770,8 @@ def execute( private_key=terminal_key, verified_params=params, dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, - closure_authority=closure_authority, loss_code="runner_exception", + snapshot=loss_snapshot, ) ) except Exception as terminal_exc: @@ -2768,6 +2814,13 @@ def execute( try: if closure_authority is None: raise ValueError("hosted result-loss closure authority is absent") + loss_snapshot = self._retain_managed_result_loss_closure( + dispatch=parsed, + run_dir=run_dir, + dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, + closure_authority=closure_authority, + loss_code="report_missing", + ) terminal_key, terminal_qualification = ( self._revalidate_terminal_authority( dispatch=parsed, @@ -2786,8 +2839,8 @@ def execute( private_key=terminal_key, verified_params=params, dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, - closure_authority=closure_authority, loss_code="report_missing", + snapshot=loss_snapshot, ) ) except Exception as terminal_exc: @@ -2865,6 +2918,40 @@ def execute( outcome = TransactionOutcome.RECONCILIATION_REQUIRED proof = None terminalization_error = exc + try: + if closure_authority is None: + raise ValueError("hosted result-loss closure authority is absent") + loss_snapshot = self._retain_managed_result_loss_closure( + dispatch=parsed, + run_dir=run_dir, + dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, + closure_authority=closure_authority, + loss_code="report_invalid", + ) + terminal_key, terminal_qualification = ( + self._revalidate_terminal_authority( + dispatch=parsed, + runner_config=runner_config, + configured_origin=configured_origin, + initial_qualification=qualification, + initial_deployment_bytes=deployment_bytes, + ) + ) + proof, report_digest, report = self._produce_managed_result_loss( + dispatch=parsed, + workflow=verified.workflow, + run_dir=run_dir, + qualification=terminal_qualification, + private_key=terminal_key, + verified_params=params, + dispatch_binding_sha256=verified.payload.dispatch_binding_sha256, + loss_code="report_invalid", + snapshot=loss_snapshot, + ) + except Exception as loss_exc: # noqa: BLE001 - remains fenced/reconciling + terminalization_error = loss_exc + else: + terminalization_error = None else: terminalization_error = None if proof is None and outcome in { diff --git a/openadapt_flow/terminal_verification_v2.py b/openadapt_flow/terminal_verification_v2.py index abaa2dbf..34bf6fa7 100644 --- a/openadapt_flow/terminal_verification_v2.py +++ b/openadapt_flow/terminal_verification_v2.py @@ -2683,6 +2683,8 @@ def _closed_terminal_outcome(self) -> "ProductionTerminalVerificationPayload": != (chain.pending or chain.entries[0]).run_request_sha256 or result_loss.pending_action_request_sha256 != (pending.action_request_sha256 if pending is not None else None) + or outcome.external_network_calls != "observed" + or receipt.external_network_calls != "observed" or manifests.identity.required != 0 or manifests.identity.confirmed != 0 or manifests.identity.results diff --git a/public-artifacts.json b/public-artifacts.json index 69322eac..655d443a 100644 --- a/public-artifacts.json +++ b/public-artifacts.json @@ -5781,7 +5781,7 @@ }, { "path": "tests/fixtures/terminal_verification_v2_terminal_vectors.json", - "sha256": "0e94c1b106ca06ef6978c5923770b6b4ae32661deb68ce81078fede9a611dd5f" + "sha256": "2c2af93f1cd5d7b62ede33e59475aa61126cdc9c568949e1a8722b0d899bee3f" }, { "path": "tests/fixtures/theme-region-stable-overhalt-v1.16.1.json", diff --git a/scripts/generate_terminal_verification_v2_vectors.py b/scripts/generate_terminal_verification_v2_vectors.py index e02cb3c2..b42e75c4 100644 --- a/scripts/generate_terminal_verification_v2_vectors.py +++ b/scripts/generate_terminal_verification_v2_vectors.py @@ -78,23 +78,12 @@ def _vector(name: str, *, uncertain_delivery: bool) -> dict[str, object]: } -def build_fixture() -> dict[str, object]: - private_key = _private_key() - private_bytes = private_key.private_bytes( - encoding=serialization.Encoding.Raw, - format=serialization.PrivateFormat.Raw, - encryption_algorithm=serialization.NoEncryption(), - ) - public_bytes = private_key.public_key().public_bytes( - encoding=serialization.Encoding.Raw, - format=serialization.PublicFormat.Raw, - ) - managed = _managed_result_loss_payload() +def _result_loss_closure_vector(payload) -> dict[str, object]: request = _result_loss_request() - closure = managed.delivery_result_loss_closure + closure = payload.delivery_result_loss_closure assert closure is not None closure_raw = canonical_json(closure) - chain_raw = canonical_json(managed.permit_chain) + chain_raw = canonical_json(payload.permit_chain) closure_result = { "schema_version": ( "openadapt.production-delivery-result-loss-closure-result/v2" @@ -103,8 +92,46 @@ def build_fixture() -> dict[str, object]: "closure_artifact_bytes_base64": b64encode(closure_raw).decode("ascii"), "closure_artifact_sha256": closure.artifact_sha256(), "permit_chain_bytes_base64": b64encode(chain_raw).decode("ascii"), - "permit_chain_sha256": managed.permit_chain.permit_chain_sha256, + "permit_chain_sha256": payload.permit_chain.permit_chain_sha256, } + return { + "http_method": "POST", + "http_route": "/api/internal/managed-delivery-result-loss-closure", + "authorization_credential_source": "hosted_dispatch.lease_token", + "request_digest_domain_base64": b64encode( + RESULT_LOSS_CLOSURE_REQUEST_DOMAIN + ).decode("ascii"), + "request_canonical_base64": b64encode(request.canonical_bytes()).decode( + "ascii" + ), + "request_sha256": request.request_sha256(), + "payload_signature_domain_base64": b64encode( + RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN + ).decode("ascii"), + "closure_payload_sha256": closure.payload_sha256, + "closure_artifact_canonical_base64": b64encode(closure_raw).decode("ascii"), + "closure_artifact_sha256": closure.artifact_sha256(), + "permit_chain_canonical_base64": b64encode(chain_raw).decode("ascii"), + "permit_chain_sha256": payload.permit_chain.permit_chain_sha256, + "result_canonical_base64": b64encode(canonical_json(closure_result)).decode( + "ascii" + ), + } + + +def build_fixture() -> dict[str, object]: + private_key = _private_key() + private_bytes = private_key.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), + ) + public_bytes = private_key.public_key().public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + managed = _managed_result_loss_payload() + managed_acknowledged = _managed_result_loss_acknowledged_payload() return { "key_id": evidence_runner_key_id(public_bytes), "private_key_base64": b64encode(private_bytes).decode("ascii"), @@ -112,29 +139,10 @@ def build_fixture() -> dict[str, object]: "schema_version": "openadapt.production-terminal-cross-language-vectors/v2", "signature_domain_base64": b64encode(SIGNATURE_DOMAIN).decode("ascii"), "signer_sha256": evidence_runner_signer_sha256(public_bytes), - "managed_result_loss_closure_vector": { - "http_method": "POST", - "http_route": "/api/internal/managed-delivery-result-loss-closure", - "authorization_credential_source": "hosted_dispatch.lease_token", - "request_digest_domain_base64": b64encode( - RESULT_LOSS_CLOSURE_REQUEST_DOMAIN - ).decode("ascii"), - "request_canonical_base64": b64encode(request.canonical_bytes()).decode( - "ascii" - ), - "request_sha256": request.request_sha256(), - "payload_signature_domain_base64": b64encode( - RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN - ).decode("ascii"), - "closure_payload_sha256": closure.payload_sha256, - "closure_artifact_canonical_base64": b64encode(closure_raw).decode("ascii"), - "closure_artifact_sha256": closure.artifact_sha256(), - "permit_chain_canonical_base64": b64encode(chain_raw).decode("ascii"), - "permit_chain_sha256": managed.permit_chain.permit_chain_sha256, - "result_canonical_base64": b64encode(canonical_json(closure_result)).decode( - "ascii" - ), - }, + "managed_result_loss_closure_vector": _result_loss_closure_vector(managed), + "managed_result_loss_acknowledged_closure_vector": ( + _result_loss_closure_vector(managed_acknowledged) + ), "vectors": [ _vector( "verified-complete", diff --git a/tests/fixtures/terminal_verification_v2_terminal_vectors.json b/tests/fixtures/terminal_verification_v2_terminal_vectors.json index 5b65c8fb..5c7d9548 100644 --- a/tests/fixtures/terminal_verification_v2_terminal_vectors.json +++ b/tests/fixtures/terminal_verification_v2_terminal_vectors.json @@ -1,5 +1,20 @@ { "key_id": "evidence-runner-ed25519-56475aa75463474c", + "managed_result_loss_acknowledged_closure_vector": { + "authorization_credential_source": "hosted_dispatch.lease_token", + "closure_artifact_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjEsImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImF1dGhlbnRpY2F0ZWRfcnVubmVyX2lkX3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJhdXRoZW50aWNhdGVkX3Nlc3Npb25faWRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImJsaW5kX3JldHJ5X2F1dGhvcml6ZWQiOmZhbHNlLCJjaGlsZF9zdGFydF9ldmlkZW5jZV9zaGEyNTYiOiJlYzY5MDc5MTg4NmFlMmNlMDBjNGU1MTE1NGMzZjEyMGVhNDQxOWVkMjY1MWUwNzNkYjU3MjQ5MThhM2IwOGM2IiwiY2hpbGRfc3RhcnRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiY2xvc2VkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJjbG9zdXJlX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDExIiwiY2xvc3VyZV9yZXF1ZXN0X3NoYTI1NiI6ImQ5MzBhYjFhYmMyYjJhMDRiODQ4Yjk1NWZmZGZmZmExZmJlMDY1NWE1NTMyNWUwNDg5YWUxMWVkODQyMDIyNjAiLCJjbG9zdXJlX3NlcXVlbmNlIjoxLCJkZWxpdmVyeV9hY2tub3dsZWRnbWVudF9hdXRob3JpemVkIjpmYWxzZSwiZGVsaXZlcnlfc3RhdGUiOiJDTE9TRURfVU5SRVNPTFZFRF9SRVNVTFRfTE9TUyIsImRpc3BhdGNoX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA5IiwiZGlzcGF0Y2hfc2Vzc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsImVmZmVjdF9hYnNlbmNlX2NsYWltZWQiOmZhbHNlLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImZpbmFsX2F1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZmluYWxfaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiZmluYWxfcnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6OSwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW1wb3RlbmN5X2tleV9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuYWdlZF9kaXNwYXRjaF9iaW5kaW5nX3NoYTI1NiI6InNoYTI1NjpjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwibmV3X3Blcm1pdF9hdXRob3JpemVkIjpmYWxzZSwibm90X3JlY2VpdmVkX2NsYWltZWQiOmZhbHNlLCJwZW5kaW5nX2FjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6bnVsbCwicGVuZGluZ19wZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjpudWxsLCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MCwicGVybWl0X2NoYWluX3NoYTI1NiI6IjUwNDhmZTk0NTg4MmQyODAyNjAwOGQ3ZTkxMzdhZDAxNmZkZDAxYWVhZjU4ODQ4MWZiNWJkMGYyNDJhNTAwNDMiLCJwZXJtaXRfY291bnQiOjEsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6bnVsbCwicmVzdWx0X2xvc3Nfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW5fc3RvcmVfaWRlbnRpdHlfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1wYXlsb2FkL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfY2FsbGJhY2tfcmVxdWlyZWQiOnRydWV9LCJwYXlsb2FkX3NoYTI1NiI6IjZiNzVmOGI0NWE3ZDNlOWNmZTg1NjRmZmUzMmYyOWMyYjU5N2MyODEzNmE5N2E0ZjU0NTNmZTJhZmQxNzI4YWEiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJrenVSMnFyY0dUNHBvNi15emRZRGdoM3dhWWpiOVdwUUdVX0JuUUxIVEliZGQ1eEJsclVTbDZINGNlVkMzM3IxQndiRnRSUGFQdm8wNXBneGdFNHdBdyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fQ==", + "closure_artifact_sha256": "b2d92f3248f4da61edb027cf59da5395ffc418ead6d485c29df6c35bc21a930d", + "closure_payload_sha256": "6b75f8b45a7d3e9cfe8564ffe32f29c2b597c28136a97a4f5453fe2afd1728aa", + "http_method": "POST", + "http_route": "/api/internal/managed-delivery-result-loss-closure", + "payload_signature_domain_base64": "b3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1wYXlsb2FkLnYyAA==", + "permit_chain_canonical_base64": "eyJlbnRyaWVzIjpbeyJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiZGVsaXZlcmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMVoiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4Iiwib25lX3VzZV9jbGFpbV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsInBlcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5IiwicGVybWl0X2lkIjoicGVybWl0OjEiLCJydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6IjhhOGY2ZDg4NzUwY2JiNmIzYmQ2NjIwOTNhZDI5ZTJiYzIwYzZhYjY5OTFkNzBlNTg4NjkxYzhiM2FiNjYyYjkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJBT0txQ2dTMXMwWGEtaXlFOTZWLXlsUHpRY3YydXZPc3V5Z0h6dnUyUnlWN0ZMc0hGX0xWZnF5eDR1MnB1bWxFbDBZazVZdTcwSlZVa3NacGFVcEREdyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwiZGVsaXZlcnlfcmVjZWlwdF9hcnRpZmFjdF9zaGEyNTYiOiJjMDBhMWJhNzAxNWYzY2JlMDVhNzZhZWFiZmY2MTlkNzkzZTczNzVhMzEwZTE5NWZjYmJkMmQyOTQ4ZjJiMjE0IiwicGVybWl0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJwZXJtaXRfaWQiOiJwZXJtaXQ6MSIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2NoZWNrZWRfYXQiOiIyMDI2LTA4LTE4VDExOjU5OjMwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2V4cGlyZXNfYXQiOiIyMDI2LTA4LTIwVDExOjAwOjAwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiJjODdmNTc1MWFhOWIyNzljY2UyM2FhMTc4YWZmOTU2ZTE5ZjY5MGZjZDJmOWY3MGUwZDk5OTc4MmE2OTBkNmI4Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiIwQnlIZ3FEeFFacm5nTElGLTZ0Wk9xOWxYYW1jQVFDMGRvVVRuZUtOUGVTLVN5b09iUXlPa0F1UEZLNVBZY056T2JaeGdzZ0FMZFdhSV9zYVVDcGxCZyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwicGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi1lbnRyeS92MiJ9XSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjUwNDhmZTk0NTg4MmQyODAyNjAwOGQ3ZTkxMzdhZDAxNmZkZDAxYWVhZjU4ODQ4MWZiNWJkMGYyNDJhNTAwNDMiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi92MiJ9", + "permit_chain_sha256": "5048fe945882d28026008d7e9137ad016fdd01aeaf588481fb5bd0f242a50043", + "request_canonical_base64": "eyJjaGlsZF9zdGFydF9ldmlkZW5jZSI6eyJhdXRoZW50aWNhdGVkX3J1bm5lcl9pZF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiYXV0aGVudGljYXRlZF9zZXNzaW9uX2lkX3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJkaXNwYXRjaF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOSIsImRpc3BhdGNoX3Nlc3Npb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTAiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsIm1hbmFnZWRfZGlzcGF0Y2hfYmluZGluZ19zaGEyNTYiOiJzaGEyNTY6Y2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsIm1hcmtlcl9zaGEyNTYiOiJlYzY5MDc5MTg4NmFlMmNlMDBjNGU1MTE1NGMzZjEyMGVhNDQxOWVkMjY1MWUwNzNkYjU3MjQ5MThhM2IwOGM2IiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3N0b3JlX2lkZW50aXR5X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5tYW5hZ2VkLWNoaWxkLXN0YXJ0L3YxIiwic3RhcnRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIn0sImV4cGVjdGVkX2Nsb3N1cmVfc2VxdWVuY2UiOjAsInJlc3VsdF9sb3NzX29ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtcmVxdWVzdC92MiJ9", + "request_digest_domain_base64": "b3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1yZXF1ZXN0LnYyAA==", + "request_sha256": "d930ab1abc2b2a04b848b955ffdfffa1fbe0655a55325e0489ae11ed84202260", + "result_canonical_base64": "eyJjbG9zdXJlX2FydGlmYWN0X2J5dGVzX2Jhc2U2NCI6ImV5SndZWGxzYjJGa0lqcDdJbUZqYTI1dmQyeGxaR2RsWkY5d1pYSnRhWFJmWTI5MWJuUWlPakVzSW1GamRIVmhkR2x2Ymw5eVpYQnNZWGxmWVhWMGFHOXlhWHBsWkNJNlptRnNjMlVzSW1GMWRHaGxiblJwWTJGMFpXUmZjblZ1Ym1WeVgybGtYM05vWVRJMU5pSTZJbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUlpTENKaGRYUm9aVzUwYVdOaGRHVmtYM05sYzNOcGIyNWZhV1JmYzJoaE1qVTJJam9pWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWXlJc0ltSnNhVzVrWDNKbGRISjVYMkYxZEdodmNtbDZaV1FpT21aaGJITmxMQ0pqYUdsc1pGOXpkR0Z5ZEY5bGRtbGtaVzVqWlY5emFHRXlOVFlpT2lKbFl6WTVNRGM1TVRnNE5tRmxNbU5sTURCak5HVTFNVEUxTkdNelpqRXlNR1ZoTkRReE9XVmtNalkxTVdVd056TmtZalUzTWpRNU1UaGhNMkl3T0dNMklpd2lZMmhwYkdSZmMzUmhjblJsWkY5aGRDSTZJakl3TWpZdE1EZ3RNVGhVTVRJNk1EQTZNREJhSWl3aVkyeHZjMlZrWDJGMElqb2lNakF5Tmkwd09DMHhPRlF4TWpvd01Eb3dNbG9pTENKamJHOXpkWEpsWDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01ERXhJaXdpWTJ4dmMzVnlaVjl5WlhGMVpYTjBYM05vWVRJMU5pSTZJbVE1TXpCaFlqRmhZbU15WWpKaE1EUmlPRFE0WWprMU5XWm1aR1ptWm1FeFptSmxNRFkxTldFMU5UTXlOV1V3TkRnNVlXVXhNV1ZrT0RReU1ESXlOakFpTENKamJHOXpkWEpsWDNObGNYVmxibU5sSWpveExDSmtaV3hwZG1WeWVWOWhZMnR1YjNkc1pXUm5iV1Z1ZEY5aGRYUm9iM0pwZW1Wa0lqcG1ZV3h6WlN3aVpHVnNhWFpsY25sZmMzUmhkR1VpT2lKRFRFOVRSVVJmVlU1U1JWTlBURlpGUkY5U1JWTlZURlJmVEU5VFV5SXNJbVJwYzNCaGRHTm9YMmxrSWpvaU1EQXdNREF3TURBdE1EQXdNQzAwTURBd0xUZ3dNREF0TURBd01EQXdNREF3TURBNUlpd2laR2x6Y0dGMFkyaGZjMlZ6YzJsdmJsOXBaQ0k2SWpBd01EQXdNREF3TFRBd01EQXROREF3TUMwNE1EQXdMVEF3TURBd01EQXdNREF4TUNJc0ltVm1abVZqZEY5aFluTmxibU5sWDJOc1lXbHRaV1FpT21aaGJITmxMQ0psZUdWamRYUnBiMjVmWVhWMGFHOXlhWFI1WDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01EQTRJaXdpWlhobFkzVjBhVzl1WDJGMWRHaHZjbWwwZVY5emFHRXlOVFlpT2lKaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaElpd2laWGhsWTNWMGFXOXVYMkYxZEdodmNtbDBlVjl6YVdkdVpYSmZjMmhoTWpVMklqb2lNalJtTm1Wa05tRmpZbVpsTVRBd09XTXdNekJrTjJOaE5UWTNZek16WTJFME9ETXdPVEV4TkRrNE1qTTJZalUxTmpGaE5tTTRNbUZpWldNMVpHVXlPQ0lzSW1acGJtRnNYMkYxZEdodmNtbDBlVjl6WlhGMVpXNWpaU0k2TUN3aVptbHVZV3hmYVc1d2RYUmZaV1JuWlY5elpYRjFaVzVqWlNJNk1Td2labWx1WVd4ZmNuVnVkR2x0WlY5a1pXeHBkbVZ5ZVY5elpYRjFaVzVqWlNJNk9Td2labXh2ZDE5eWRXNWZhV1JmYzJoaE1qVTJJam9pTVRGbE5UazBaalE0TVRrMU9HTXhNR1V6TURFMVpEQmlaakEwTkRkaE1qSm1NRFk0WVRoaE5qUTNaalEzTldSbU1UVmpaVEpqTjJGaU5HSTRaak5tTVNJc0ltbGtaVzF3YjNSbGJtTjVYMnRsZVY5emFHRXlOVFlpT2lKa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa0lpd2liV0Z1WVdkbFpGOWthWE53WVhSamFGOWlhVzVrYVc1blgzTm9ZVEkxTmlJNkluTm9ZVEkxTmpwalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOaklpd2libVYzWDNCbGNtMXBkRjloZFhSb2IzSnBlbVZrSWpwbVlXeHpaU3dpYm05MFgzSmxZMlZwZG1Wa1gyTnNZV2x0WldRaU9tWmhiSE5sTENKd1pXNWthVzVuWDJGamRHbHZibDl5WlhGMVpYTjBYM05vWVRJMU5pSTZiblZzYkN3aWNHVnVaR2x1WjE5d1pYSnRhWFJmWVhKMGFXWmhZM1JmYzJoaE1qVTJJanB1ZFd4c0xDSndaVzVrYVc1blgzQmxjbTFwZEY5amIzVnVkQ0k2TUN3aWNHVnliV2wwWDJOb1lXbHVYM05vWVRJMU5pSTZJalV3TkRobVpUazBOVGc0TW1ReU9EQXlOakF3T0dRM1pUa3hNemRoWkRBeE5tWmtaREF4WVdWaFpqVTRPRFE0TVdaaU5XSmtNR1l5TkRKaE5UQXdORE1pTENKd1pYSnRhWFJmWTI5MWJuUWlPakVzSW5KbFkyVnBjSFJmWVdKelpXNWpaVjl2WW5ObGNuWmxaRjloZENJNmJuVnNiQ3dpY21WemRXeDBYMnh2YzNOZmIySnpaWEoyWldSZllYUWlPaUl5TURJMkxUQTRMVEU0VkRFeU9qQXdPakF5V2lJc0luSjFibDlwWkNJNklqQXdNREF3TURBd0xUQXdNREF0TkRBd01DMDRNREF3TFRBd01EQXdNREF3TURBd01TSXNJbkoxYmw5eVpYRjFaWE4wWDNOb1lUSTFOaUk2SW1OalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJNaUxDSnlkVzVmYzNSdmNtVmZhV1JsYm5ScGRIbGZjMmhoTWpVMklqb2lZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZaUlzSW5OamFHVnRZVjkyWlhKemFXOXVJam9pYjNCbGJtRmtZWEIwTG5CeWIyUjFZM1JwYjI0dFpHVnNhWFpsY25rdGNtVnpkV3gwTFd4dmMzTXRZMnh2YzNWeVpTMXdZWGxzYjJGa0wzWXlJaXdpZEdWdVlXNTBYMmxrSWpvaU1EQXdNREF3TURBdE1EQXdNQzAwTURBd0xUZ3dNREF0TURBd01EQXdNREF3TURBeUlpd2lkR1Z5YldsdVlXeGZZMkZzYkdKaFkydGZjbVZ4ZFdseVpXUWlPblJ5ZFdWOUxDSndZWGxzYjJGa1gzTm9ZVEkxTmlJNklqWmlOelZtT0dJME5XRTNaRE5sT1dObVpUZzFOalJtWm1Vek1tWXlPV015WWpVNU4yTXlPREV6Tm1FNU4yRTBaalUwTlRObVpUSmhabVF4TnpJNFlXRWlMQ0p6WTJobGJXRmZkbVZ5YzJsdmJpSTZJbTl3Wlc1aFpHRndkQzV3Y205a2RXTjBhVzl1TFdSbGJHbDJaWEo1TFhKbGMzVnNkQzFzYjNOekxXTnNiM04xY21VdFlYSjBhV1poWTNRdmRqSWlMQ0p6YVdkdVlYUjFjbVVpT2lKcmVuVlNNbkZ5WTBkVU5IQnZOaTE1ZW1SWlJHZG9NM2RoV1dwaU9WZHdVVWRWWDBKdVVVeElWRWxpWkdRMWVFSnNjbFZUYkRaSU5HTmxWa016TTNJeFFuZGlSblJTVUdGUWRtOHdOWEJuZUdkRk5IZEJkeUlzSW5OcFoyNWxjaUk2ZXlKaGJHZHZjbWwwYUcwaU9pSmxaREkxTlRFNUlpd2lhMlY1WDJsa0lqb2laR1ZzYVhabGNua3RZWFYwYUc5eWFYUjVMV1ZrTWpVMU1Ua3RNalJtTm1Wa05tRmpZbVpsTVRBd09TSXNJbkIxWW14cFkxOXJaWGtpT2lKTFlYazJORlZIT0hsMlEzbE1hSEZWTURBd1RIaDZXV1ZWYlRCTUwyaE1TV3cxVXpocmVVdFhZbVJqUFNKOWZRPT0iLCJjbG9zdXJlX2FydGlmYWN0X3NoYTI1NiI6ImIyZDkyZjMyNDhmNGRhNjFlZGIwMjdjZjU5ZGE1Mzk1ZmZjNDE4ZWFkNmQ0ODVjMjlkZjZjMzViYzIxYTkzMGQiLCJwZXJtaXRfY2hhaW5fYnl0ZXNfYmFzZTY0IjoiZXlKbGJuUnlhV1Z6SWpwYmV5SmtaV3hwZG1WeWVWOXlaV05sYVhCMFgyRnlkR2xtWVdOMElqcDdJbkJoZVd4dllXUWlPbnNpWVhWMGFHVnVkR2xqWVhSbFpGOXlkVzV1WlhKZmFXUmZjMmhoTWpVMklqb2lZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZbUppWW1KaVltSmlZaUlzSW1GMWRHaGxiblJwWTJGMFpXUmZjMlZ6YzJsdmJsOXBaRjl6YUdFeU5UWWlPaUpqWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qSWl3aVpHVnNhWFpsY21Wa1gyRjBJam9pTWpBeU5pMHdPQzB4T0ZReE1qb3dNRG93TVZvaUxDSmxlR1ZqZFhScGIyNWZZWFYwYUc5eWFYUjVYMmxrSWpvaU1EQXdNREF3TURBdE1EQXdNQzAwTURBd0xUZ3dNREF0TURBd01EQXdNREF3TURBNElpd2liMjVsWDNWelpWOWpiR0ZwYlY5cFpDSTZJakF3TURBd01EQXdMVEF3TURBdE5EQXdNQzA0TURBd0xUQXdNREF3TURBd01EQXhNQ0lzSW5CbGNtMXBkRjloY25ScFptRmpkRjl6YUdFeU5UWWlPaUk1TnpKaE9ESmhaalExTm1SaE5UUXlZbUZqTnpnME5qQXhPRGc1TmpZNE9Ea3haRGhpTjJOalpESTBNamt3TkRneE5tSXpNamt5WlRFMU4yVTFNamc1SWl3aWNHVnliV2wwWDJsa0lqb2ljR1Z5YldsME9qRWlMQ0p5ZFc1MGFXMWxYMlJsYkdsMlpYSjVYM05sY1hWbGJtTmxJam81TENKelkyaGxiV0ZmZG1WeWMybHZiaUk2SW05d1pXNWhaR0Z3ZEM1d2NtOWtkV04wYVc5dUxXUmxiR2wyWlhKNUxYSmxZMlZwY0hRdGNHRjViRzloWkM5Mk1pSjlMQ0p3WVhsc2IyRmtYM05vWVRJMU5pSTZJamhoT0dZMlpEZzROelV3WTJKaU5tSXpZbVEyTmpJd09UTmhaREk1WlRKaVl6SXdZelpoWWpZNU9URmtOekJsTlRnNE5qa3hZemhpTTJGaU5qWXlZamtpTENKelkyaGxiV0ZmZG1WeWMybHZiaUk2SW05d1pXNWhaR0Z3ZEM1d2NtOWtkV04wYVc5dUxXUmxiR2wyWlhKNUxYSmxZMlZwY0hRdFlYSjBhV1poWTNRdmRqSWlMQ0p6YVdkdVlYUjFjbVVpT2lKQlQwdHhRMmRUTVhNd1dHRXRhWGxGT1RaV0xYbHNVSHBSWTNZeWRYWlBjM1Y1WjBoNmRuVXlVbmxXTjBaTWMwaEdYMHhXWm5GNWVEUjFNbkIxYld4RmJEQlphelZaZFRjd1NsWlZhM05hY0dGVmNFUkVkeUlzSW5OcFoyNWxjaUk2ZXlKaGJHZHZjbWwwYUcwaU9pSmxaREkxTlRFNUlpd2lhMlY1WDJsa0lqb2laR1ZzYVhabGNua3RZWFYwYUc5eWFYUjVMV1ZrTWpVMU1Ua3RNalJtTm1Wa05tRmpZbVpsTVRBd09TSXNJbkIxWW14cFkxOXJaWGtpT2lKTFlYazJORlZIT0hsMlEzbE1hSEZWTURBd1RIaDZXV1ZWYlRCTUwyaE1TV3cxVXpocmVVdFhZbVJqUFNKOWZTd2laR1ZzYVhabGNubGZjbVZqWldsd2RGOWhjblJwWm1GamRGOXphR0V5TlRZaU9pSmpNREJoTVdKaE56QXhOV1l6WTJKbE1EVmhOelpoWldGaVptWTJNVGxrTnprelpUY3pOelZoTXpFd1pURTVOV1pqWW1Ka01tUXlPVFE0WmpKaU1qRTBJaXdpY0dWeWJXbDBYMkZ5ZEdsbVlXTjBJanA3SW5CaGVXeHZZV1FpT25zaVlXTjBhVzl1WDNKbGNYVmxjM1JmYzJoaE1qVTJJam9pWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkNJc0ltRmtiV2x6YzJsdmJsOWhjblJwWm1GamRGOXphR0V5TlRZaU9pSmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtaR1JrWkdSa1pHUmtJaXdpWVhWMGFHOXlhWFI1WDNObGNYVmxibU5sSWpvd0xDSmxiblpwY205dWJXVnVkRjlrYVdkbGMzUWlPaUpoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoSWl3aVpYWnBaR1Z1WTJWZmFXUmxiblJwZEhsZmMyaGhNalUySWpvaVpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpTSXNJbVY0WldOMWRHbHZibDloZFhSb2IzSnBkSGxmYVdRaU9pSXdNREF3TURBd01DMHdNREF3TFRRd01EQXRPREF3TUMwd01EQXdNREF3TURBd01EZ2lMQ0psZUdWamRYUnBiMjVmWVhWMGFHOXlhWFI1WDNOb1lUSTFOaUk2SW1GaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdGaFlXRmhZV0ZoWVdFaUxDSm1iRzkzWDNKMWJsOXBaRjl6YUdFeU5UWWlPaUl4TVdVMU9UUm1ORGd4T1RVNFl6RXdaVE13TVRWa01HSm1NRFEwTjJFeU1tWXdOamhoT0dFMk5EZG1ORGMxWkdZeE5XTmxNbU0zWVdJMFlqaG1NMll4SWl3aWFXNXdkWFJmWldSblpWOXpaWEYxWlc1alpTSTZNU3dpYVhOemRXVmtYMkYwSWpvaU1qQXlOaTB3T0MweE9GUXhNam93TURvd01Gb2lMQ0p3WlhKdGFYUmZhV1FpT2lKd1pYSnRhWFE2TVNJc0luRjFZV3hwWm1sallYUnBiMjVmYzJsbmJtVnlYM0psWjJsemRISjVYMk5vWldOclpXUmZZWFFpT2lJeU1ESTJMVEE0TFRFNFZERXhPalU1T2pNd1dpSXNJbkYxWVd4cFptbGpZWFJwYjI1ZmMybG5ibVZ5WDNKbFoybHpkSEo1WDJWNGNHbHlaWE5mWVhRaU9pSXlNREkyTFRBNExUSXdWREV4T2pBd09qQXdXaUlzSW5GMVlXeHBabWxqWVhScGIyNWZjMmxuYm1WeVgzSmxaMmx6ZEhKNVgzSmxkbWx6YVc5dUlqbzNMQ0p4ZFdGc2FXWnBZMkYwYVc5dVgzTnBaMjVsY2w5eVpXZHBjM1J5ZVY5emFHRXlOVFlpT2lKbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbFpXVmxaV1ZsWldWbElpd2ljblZ1WDJsa0lqb2lNREF3TURBd01EQXRNREF3TUMwME1EQXdMVGd3TURBdE1EQXdNREF3TURBd01EQXhJaXdpY25WdVgzSmxjWFZsYzNSZmMyaGhNalUySWpvaVkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOalkyTmpZMk5qWTJOall5SXNJbk5qYUdWdFlWOTJaWEp6YVc5dUlqb2liM0JsYm1Ga1lYQjBMbkJ5YjJSMVkzUnBiMjR0WkdWc2FYWmxjbmt0Y0dWeWJXbDBMWEJoZVd4dllXUXZkaklpZlN3aWNHRjViRzloWkY5emFHRXlOVFlpT2lKak9EZG1OVGMxTVdGaE9XSXlOemxqWTJVeU0yRmhNVGM0WVdabU9UVTJaVEU1WmpZNU1HWmpaREptT1dZM01HVXdaRGs1T1RjNE1tRTJPVEJrTm1JNElpd2ljMk5vWlcxaFgzWmxjbk5wYjI0aU9pSnZjR1Z1WVdSaGNIUXVjSEp2WkhWamRHbHZiaTFrWld4cGRtVnllUzF3WlhKdGFYUXRZWEowYVdaaFkzUXZkaklpTENKemFXZHVZWFIxY21VaU9pSXdRbmxJWjNGRWVGRmFjbTVuVEVsR0xUWjBXazl4T1d4WVlXMWpRVkZETUdSdlZWUnVaVXRPVUdWVExWTjViMDlpVVhsUGEwRjFVRVpMTlZCWlkwNTZUMkphZUdkelowRk1aRmRoU1Y5ellWVkRjR3hDWnlJc0luTnBaMjVsY2lJNmV5SmhiR2R2Y21sMGFHMGlPaUpsWkRJMU5URTVJaXdpYTJWNVgybGtJam9pWkdWc2FYWmxjbmt0WVhWMGFHOXlhWFI1TFdWa01qVTFNVGt0TWpSbU5tVmtObUZqWW1abE1UQXdPU0lzSW5CMVlteHBZMTlyWlhraU9pSkxZWGsyTkZWSE9IbDJRM2xNYUhGVk1EQXdUSGg2V1dWVmJUQk1MMmhNU1d3MVV6aHJlVXRYWW1SalBTSjlmU3dpY0dWeWJXbDBYMkZ5ZEdsbVlXTjBYM05vWVRJMU5pSTZJamszTW1FNE1tRm1ORFUyWkdFMU5ESmlZV00zT0RRMk1ERTRPRGsyTmpnNE9URmtPR0kzWTJOa01qUXlPVEEwT0RFMllqTXlPVEpsTVRVM1pUVXlPRGtpTENKelkyaGxiV0ZmZG1WeWMybHZiaUk2SW05d1pXNWhaR0Z3ZEM1d2NtOWtkV04wYVc5dUxXUmxiR2wyWlhKNUxYQmxjbTFwZEMxamFHRnBiaTFsYm5SeWVTOTJNaUo5WFN3aWNHVnliV2wwWDJOb1lXbHVYM05vWVRJMU5pSTZJalV3TkRobVpUazBOVGc0TW1ReU9EQXlOakF3T0dRM1pUa3hNemRoWkRBeE5tWmtaREF4WVdWaFpqVTRPRFE0TVdaaU5XSmtNR1l5TkRKaE5UQXdORE1pTENKelkyaGxiV0ZmZG1WeWMybHZiaUk2SW05d1pXNWhaR0Z3ZEM1d2NtOWtkV04wYVc5dUxXUmxiR2wyWlhKNUxYQmxjbTFwZEMxamFHRnBiaTkyTWlKOSIsInBlcm1pdF9jaGFpbl9zaGEyNTYiOiI1MDQ4ZmU5NDU4ODJkMjgwMjYwMDhkN2U5MTM3YWQwMTZmZGQwMWFlYWY1ODg0ODFmYjViZDBmMjQyYTUwMDQzIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZXN1bHQtbG9zcy1jbG9zdXJlLXJlc3VsdC92MiIsInN0YXR1cyI6ImNsb3NlZCJ9" + }, "managed_result_loss_closure_vector": { "authorization_credential_source": "hosted_dispatch.lease_token", "closure_artifact_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImF1dGhlbnRpY2F0ZWRfcnVubmVyX2lkX3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJhdXRoZW50aWNhdGVkX3Nlc3Npb25faWRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImJsaW5kX3JldHJ5X2F1dGhvcml6ZWQiOmZhbHNlLCJjaGlsZF9zdGFydF9ldmlkZW5jZV9zaGEyNTYiOiJlYzY5MDc5MTg4NmFlMmNlMDBjNGU1MTE1NGMzZjEyMGVhNDQxOWVkMjY1MWUwNzNkYjU3MjQ5MThhM2IwOGM2IiwiY2hpbGRfc3RhcnRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiY2xvc2VkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJjbG9zdXJlX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDExIiwiY2xvc3VyZV9yZXF1ZXN0X3NoYTI1NiI6ImQ5MzBhYjFhYmMyYjJhMDRiODQ4Yjk1NWZmZGZmZmExZmJlMDY1NWE1NTMyNWUwNDg5YWUxMWVkODQyMDIyNjAiLCJjbG9zdXJlX3NlcXVlbmNlIjoxLCJkZWxpdmVyeV9hY2tub3dsZWRnbWVudF9hdXRob3JpemVkIjpmYWxzZSwiZGVsaXZlcnlfc3RhdGUiOiJDTE9TRURfVU5SRVNPTFZFRF9SRVNVTFRfTE9TUyIsImRpc3BhdGNoX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA5IiwiZGlzcGF0Y2hfc2Vzc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsImVmZmVjdF9hYnNlbmNlX2NsYWltZWQiOmZhbHNlLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImZpbmFsX2F1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZmluYWxfaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiZmluYWxfcnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6MCwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW1wb3RlbmN5X2tleV9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuYWdlZF9kaXNwYXRjaF9iaW5kaW5nX3NoYTI1NiI6InNoYTI1NjpjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwibmV3X3Blcm1pdF9hdXRob3JpemVkIjpmYWxzZSwibm90X3JlY2VpdmVkX2NsYWltZWQiOmZhbHNlLCJwZW5kaW5nX2FjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJwZW5kaW5nX3Blcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5IiwicGVuZGluZ19wZXJtaXRfY291bnQiOjEsInBlcm1pdF9jaGFpbl9zaGEyNTYiOiI1OWIzOTk3MWUzMDUxMDljNTQyOTk0NWE1ZmFiNDY2MGIwOWY0MWU3YWMzZmZkOTJlMzUwNmFmMDAzMzdkYTk1IiwicGVybWl0X2NvdW50IjoxLCJyZWNlaXB0X2Fic2VuY2Vfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInJlc3VsdF9sb3NzX29ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwicnVuX3N0b3JlX2lkZW50aXR5X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtcGF5bG9hZC92MiIsInRlbmFudF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMiIsInRlcm1pbmFsX2NhbGxiYWNrX3JlcXVpcmVkIjp0cnVlfSwicGF5bG9hZF9zaGEyNTYiOiJhODkxYWM0Y2JhNWZkZmMxN2QwM2MxZjgwMmEyZGY4YzExZmJmMGI2MmU0ZjA0YmU2N2YxYTVjNmU2OGQwYTUzIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZXN1bHQtbG9zcy1jbG9zdXJlLWFydGlmYWN0L3YyIiwic2lnbmF0dXJlIjoiZ2JzdDNuNUkzMEFBdXc2ajE3UkRFLUFlUWV5cXNKa1J1TWxyR0I3SklDTzhTbkx4ZUJIWkszNDVUYjdZY2NpVWY1SExfQjhWTnRKWkRUUndGaTNVRFEiLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImRlbGl2ZXJ5LWF1dGhvcml0eS1lZDI1NTE5LTI0ZjZlZDZhY2JmZTEwMDkiLCJwdWJsaWNfa2V5IjoiS2F5NjRVRzh5dkN5TGhxVTAwMEx4elllVW0wTC9oTElsNVM4a3lLV2JkYz0ifX0=", @@ -158,7 +173,7 @@ "uncertain_delivery": true }, "effect_state": null, - "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlIjp7InBheWxvYWQiOnsiYWNrbm93bGVkZ2VkX3Blcm1pdF9jb3VudCI6MCwiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6ImVjNjkwNzkxODg2YWUyY2UwMGM0ZTUxMTU0YzNmMTIwZWE0NDE5ZWQyNjUxZTA3M2RiNTcyNDkxOGEzYjA4YzYiLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJjbG9zZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsImNsb3N1cmVfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTEiLCJjbG9zdXJlX3JlcXVlc3Rfc2hhMjU2IjoiZDkzMGFiMWFiYzJiMmEwNGI4NDhiOTU1ZmZkZmZmYTFmYmUwNjU1YTU1MzI1ZTA0ODlhZTExZWQ4NDIwMjI2MCIsImNsb3N1cmVfc2VxdWVuY2UiOjEsImRlbGl2ZXJ5X2Fja25vd2xlZGdtZW50X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9pbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjowLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbXBvdGVuY3lfa2V5X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJuZXdfcGVybWl0X2F1dGhvcml6ZWQiOmZhbHNlLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsInBlbmRpbmdfYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsInBlbmRpbmdfcGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJwZXJtaXRfY291bnQiOjEsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicmVzdWx0X2xvc3Nfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW5fc3RvcmVfaWRlbnRpdHlfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1wYXlsb2FkL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfY2FsbGJhY2tfcmVxdWlyZWQiOnRydWV9LCJwYXlsb2FkX3NoYTI1NiI6ImE4OTFhYzRjYmE1ZmRmYzE3ZDAzYzFmODAyYTJkZjhjMTFmYmYwYjYyZTRmMDRiZTY3ZjFhNWM2ZTY4ZDBhNTMiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJnYnN0M241STMwQUF1dzZqMTdSREUtQWVRZXlxc0prUnVNbHJHQjdKSUNPOFNuTHhlQkhaSzM0NVRiN1ljY2lVZjVITF9COFZOdEpaRFRSd0ZpM1VEUSIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV2aWRlbmNlX2lkZW50aXR5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJldmlkZW5jZV9tYW5pZmVzdHMiOnsiYXV0aG9yaXphdGlvbiI6eyJhZG1pc3Npb25fYXJ0aWZhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNiIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJnb3Zlcm5lZF9hdXRob3JpemF0aW9uX2lkX3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJtYW5pZmVzdF9zaGEyNTYiOiIxNWMwZTc2ZGMyZTMyMWEzODRlNTNlYzE1ZmY1M2U3NTU1N2I3Y2RlNzFjNWFkMmJhMmI5M2ZmMmU5NTgzNWE1IiwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWF1dGhvcml6YXRpb24tZXZpZGVuY2UvdjEifSwiZWZmZWN0Ijp7ImNvbmZpcm1lZCI6MCwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJtYW5pZmVzdF9zaGEyNTYiOiI3ZTEyZGNhNWM0NGNmOTk4YTQ2OTM2MmI4NjJlODE2YTdkMmM4NTQxNjNmNjNlOGRlNjFmYjc5ZTk1YmE4MDA3IiwicmVjb3JkcyI6W10sInJlcXVpcmVkIjowLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWVmZmVjdC1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiaWRlbnRpdHkiOnsiY29uZmlybWVkIjowLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiZDNhN2VhYzM0MjdkNzZmNmJjMzYwMzA3Mzg2MDkxMTlmMDdlMGQ5MWE2ZDM1OGFiY2U1YjZmMGQ2ZGY3ODY0NSIsInJlcXVpcmVkIjowLCJyZXN1bHRzIjpbXSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1pZGVudGl0eS1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwicG9saWN5Ijp7ImFkbWlzc2lvbl9wb2xpY3lfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImVmZmVjdF9jb250cmFjdF9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZW52aXJvbm1lbnRfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJnb3Zlcm5lZF9wb2xpY3lfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImdvdmVybmVkX3J1bnRpbWVfaW5wdXRzX2RpZ2VzdCI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiNGY5ODhkMDE1YmNkN2NkN2UwNWJhMDY2M2Q4OWYwYTk2OGYwZDhhY2U5OWIwOTBlYmFiZWU1MmExYzU1MDQyMSIsIm1pbmltdW1fZWZmZWN0X3RpZXIiOjEsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9saWN5LWV2aWRlbmNlL3YxIn0sInBvc3Rjb25kaXRpb24iOnsiY29uZmlybWVkIjowLCJtYW5pZmVzdF9zaGEyNTYiOiI1MmYwOWQ1ZDYzOWIxYzM4M2YzNzc5NjNlNDg3Y2I4OWQ4OTQxNzg1N2U0NGRhN2UzZjIzZTU1YmU3NTFlN2Y0IiwicmVjb3JkcyI6W10sInJlcXVpcmVkIjowLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvc3Rjb25kaXRpb24tZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn19LCJldmlkZW5jZV9ydW5uZXJfc2lnbmVyX3NoYTI1NiI6IjU2NDc1YWE3NTQ2MzQ3NGMwMjg1ZGY1ZGJmMmJjYWI3M2RhNjUxMzU4ODM5ZTliNzc0ODFiMmVhYjEwNzcwOGMiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImV4ZWN1dGlvbl9vdXRjb21lIjp7ImNvbXBlbnNhdGlvbl9hY3Rpb25zIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iXSwiZXhlY3V0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJub25lIiwibWFuYWdlZF9yZXN1bHRfbG9zc19ldmlkZW5jZV9zaGEyNTYiOiIyY2QwNzY0OTBjYTVlNWU4ZDlkMGJkMTQ2ZDU1OWMwNmVkNDEzYTY3ZDY5NDgxYTFhZmNjMzU0NDY0ZWNkYjA3IiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJwYXNzZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjAsInBvc3Rjb25kaXRpb24iOjB9LCJwb3N0Y29uZGl0aW9uX2V2aWRlbmNlIjpbXSwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInF1YWxpZmljYXRpb25fZXZpZGVuY2Vfb25seSI6ZmFsc2UsInJlcXVpcmVkX2NvbnRyYWN0cyI6eyJhdXRob3JpemF0aW9uIjoxLCJlZmZlY3QiOjAsImlkZW50aXR5IjowLCJwb3N0Y29uZGl0aW9uIjowfSwidmVyc2lvbiI6Im9wZW5hZGFwdC5leGVjdXRpb24tb3V0Y29tZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiZXhlY3V0aW9uX291dGNvbWVfc2hhMjU2IjoiMzQ0OTYyZDJkZTM2ZWNiZDI5NWIxZTViZjhhZmIwNmE3ZjQ2NWQ2NTRlODFjZGZiNTIxNmZiYjYxMWYxZGE4YiIsImV4ZWN1dGlvbl9wdXJwb3NlIjoicHJvZHVjdGlvbiIsImZpbmFsX2F1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZmluYWxfcnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6MCwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAzWiIsIm1hbmFnZWRfcmVzdWx0X2xvc3MiOnsiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3JlcG9ydF9yZXRhaW5lZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6ImVjNjkwNzkxODg2YWUyY2UwMGM0ZTUxMTU0YzNmMTIwZWE0NDE5ZWQyNjUxZTA3M2RiNTcyNDkxOGEzYjA4YzYiLCJjaGlsZF9zdGFydGVkIjp0cnVlLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlX2FydGlmYWN0X3NoYTI1NiI6IjY1ZDBkODJkNTc3YTUzMjMwMjJiMmFmY2UxYTI2YTIxNmE1MmQyMWVmOTgyZTA1NDBjMjY2ZjMwNjNmYmM3OTciLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV2aWRlbmNlX3NoYTI1NiI6IjJjZDA3NjQ5MGNhNWU1ZThkOWQwYmQxNDZkNTU5YzA2ZWQ0MTNhNjdkNjk0ODFhMWFmY2MzNTQ0NjRlY2RiMDciLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpZGVtcG90ZW5jeV9rZXlfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImxvc3NfY29kZSI6InJlcG9ydF9taXNzaW5nIiwibWFuYWdlZF9kaXNwYXRjaF9iaW5kaW5nX3NoYTI1NiI6InNoYTI1NjpjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwibm90X3JlY2VpdmVkX2NsYWltZWQiOmZhbHNlLCJvYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicGVuZGluZ19hY3Rpb25fcmVxdWVzdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwicGVuZGluZ19wZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInJlcG9ydF9wcm92ZW5hbmNlIjoib3V0ZXJfcnVubmVyX3JlY292ZXJ5IiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bl9zdG9yZV9pZGVudGl0eV9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQubWFuYWdlZC1yZXN1bHQtbG9zcy92MSJ9LCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MSwicGVybWl0X2NoYWluIjp7ImVudHJpZXMiOltdLCJwZW5kaW5nIjp7ImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3QiOm51bGwsImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3Rfc2hhMjU2IjpudWxsLCJkZWxpdmVyeV9zdGF0ZSI6IlVOUkVTT0xWRUQiLCJwZXJtaXRfYXJ0aWZhY3QiOnsicGF5bG9hZCI6eyJhY3Rpb25fcmVxdWVzdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2FydGlmYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJhdXRob3JpdHlfc2VxdWVuY2UiOjAsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJldmlkZW5jZV9pZGVudGl0eV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsInBlcm1pdF9pZCI6InBlcm1pdDoxIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfY2hlY2tlZF9hdCI6IjIwMjYtMDgtMThUMTE6NTk6MzBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfZXhwaXJlc19hdCI6IjIwMjYtMDgtMjBUMTE6MDA6MDBaIiwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6ImM4N2Y1NzUxYWE5YjI3OWNjZTIzYWExNzhhZmY5NTZlMTlmNjkwZmNkMmY5ZjcwZTBkOTk5NzgyYTY5MGQ2YjgiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6IjBCeUhncUR4UVpybmdMSUYtNnRaT3E5bFhhbWNBUUMwZG9VVG5lS05QZVMtU3lvT2JReU9rQXVQRks1UFljTnpPYlp4Z3NnQUxkV2FJX3NhVUNwbEJnIiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJwZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZW5kaW5nLXBlcm1pdC92MiJ9LCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTliMzk5NzFlMzA1MTA5YzU0Mjk5NDVhNWZhYjQ2NjBiMDlmNDFlN2FjM2ZmZDkyZTM1MDZhZjAwMzM3ZGE5NSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWNoYWluL3YyIn0sInBlcm1pdF9jb3VudCI6MSwicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfcmV2aXNpb24iOjcsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVjZWlwdCI6eyJhdXRob3JpemF0aW9uX2NvbmZpcm1lZCI6MSwiYXV0aG9yaXphdGlvbl9yZXF1aXJlZCI6MSwiYnVuZGxlX2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJkdXJhdGlvbl9tcyI6MCwiZWZmZWN0X3RpZXJfcmVhY2hlZCI6Im5vbmUiLCJlZmZlY3RzX2NvbmZpcm1lZCI6MCwiZWZmZWN0c19yZXF1aXJlZCI6MCwiZXN0X2Nvc3RfbWljcm91c2QiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiJdLCJleHRlcm5hbF9uZXR3b3JrX2NhbGxzIjoibm9uZSIsImdlbmVyYXRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiaGVhbHMiOjAsImlkZW50aXR5X2FwcGxpY2FibGUiOjAsImlkZW50aXR5X2FybWVkIjowLCJpZGVudGl0eV9jb25maXJtZWQiOjAsImlkZW50aXR5X3JlcXVpcmVkIjowLCJtYW5hZ2VkX3Jlc3VsdF9sb3NzX2V2aWRlbmNlX3NoYTI1NiI6IjJjZDA3NjQ5MGNhNWU1ZThkOWQwYmQxNDZkNTU5YzA2ZWQ0MTNhNjdkNjk0ODFhMWFmY2MzNTQ0NjRlY2RiMDciLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsIm92ZXJfaGFsdF9jb3VudCI6MCwicG9zdGNvbmRpdGlvbnNfY29uZmlybWVkIjowLCJwb3N0Y29uZGl0aW9uc19yZXF1aXJlZCI6MCwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInByb3ZlbmFuY2UiOiJwcm9kdWN0aW9uIiwicmVjZWlwdF9idWlsZGVyX3ZlcnNpb24iOiIxLjIuMyIsInJ1bmdfaGlzdG9ncmFtIjp7fSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1ydW4tcmVjZWlwdC92MSIsInNvdXJjZV9yZWNlaXB0X2RpZ2VzdCI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzb3VyY2VfcmVjZWlwdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3NjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnJ1bi1yZXBvcnQvdjEiLCJzdGVwc19vayI6MCwic3RlcHNfdG90YWwiOjEsInN1YnN0cmF0ZSI6IndlYiIsInRyYW5zYWN0aW9uX291dGNvbWUiOiJSRUNPTkNJTElBVElPTl9SRVFVSVJFRCJ9LCJydW5fcmVjZWlwdF9zaGEyNTYiOiIwOTU1YjBlNzllMDlmYjFlMzRiZGI5OGE4ZDEzMGE4YzEyNjNiZTg2Y2E5ZDU0ODYyZDdkMzRhYmZmNGNkOWI0IiwicnVuX3JlcG9ydF9vYmplY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bl9yZXBvcnRfb2JqZWN0X3ZlcnNpb24iOiJ2ZXJzaW9uOm1hbmFnZWQtcmVzdWx0LWxvc3M6MSIsInJ1bl9yZXBvcnRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bnRpbWVfc3Vic3RyYXRlIjoid2ViIiwicnVudGltZV92YWxpZGF0aW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA1Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi10ZXJtaW5hbC12ZXJpZmljYXRpb24vdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9zZXF1ZW5jZSI6MSwidmVyaWZpZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJ3b3JrZmxvd19pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMyIsIndvcmtmbG93X3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQifSwic2lnbmF0dXJlIjoidUd0S1hWY2pxS0tyTEhPMm44dGhmYUdsbDk5bHpFeHF0MlcyUHpSZlQyOG9udXpzNjk5VGxtMDVkSzlCVTlCZC16WDM3NEJDcjBtVktIeHJ2WnRsQlEiLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImV2aWRlbmNlLXJ1bm5lci1lZDI1NTE5LTU2NDc1YWE3NTQ2MzQ3NGMiLCJwdWJsaWNfa2V5IjoiQTZFSHYvUE9FTDRkY04wWTUwdkFtV2ZrMWpDYnBRMWZIZHlHWkJKVk1iZz0ifX0=", + "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjAsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlIjp7InBheWxvYWQiOnsiYWNrbm93bGVkZ2VkX3Blcm1pdF9jb3VudCI6MCwiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6ImVjNjkwNzkxODg2YWUyY2UwMGM0ZTUxMTU0YzNmMTIwZWE0NDE5ZWQyNjUxZTA3M2RiNTcyNDkxOGEzYjA4YzYiLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJjbG9zZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsImNsb3N1cmVfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTEiLCJjbG9zdXJlX3JlcXVlc3Rfc2hhMjU2IjoiZDkzMGFiMWFiYzJiMmEwNGI4NDhiOTU1ZmZkZmZmYTFmYmUwNjU1YTU1MzI1ZTA0ODlhZTExZWQ4NDIwMjI2MCIsImNsb3N1cmVfc2VxdWVuY2UiOjEsImRlbGl2ZXJ5X2Fja25vd2xlZGdtZW50X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9pbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjowLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbXBvdGVuY3lfa2V5X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJuZXdfcGVybWl0X2F1dGhvcml6ZWQiOmZhbHNlLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsInBlbmRpbmdfYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsInBlbmRpbmdfcGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJwZW5kaW5nX3Blcm1pdF9jb3VudCI6MSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJwZXJtaXRfY291bnQiOjEsInJlY2VpcHRfYWJzZW5jZV9vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicmVzdWx0X2xvc3Nfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW5fc3RvcmVfaWRlbnRpdHlfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1wYXlsb2FkL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfY2FsbGJhY2tfcmVxdWlyZWQiOnRydWV9LCJwYXlsb2FkX3NoYTI1NiI6ImE4OTFhYzRjYmE1ZmRmYzE3ZDAzYzFmODAyYTJkZjhjMTFmYmYwYjYyZTRmMDRiZTY3ZjFhNWM2ZTY4ZDBhNTMiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlc3VsdC1sb3NzLWNsb3N1cmUtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJnYnN0M241STMwQUF1dzZqMTdSREUtQWVRZXlxc0prUnVNbHJHQjdKSUNPOFNuTHhlQkhaSzM0NVRiN1ljY2lVZjVITF9COFZOdEpaRFRSd0ZpM1VEUSIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV2aWRlbmNlX2lkZW50aXR5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJldmlkZW5jZV9tYW5pZmVzdHMiOnsiYXV0aG9yaXphdGlvbiI6eyJhZG1pc3Npb25fYXJ0aWZhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNiIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJnb3Zlcm5lZF9hdXRob3JpemF0aW9uX2lkX3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJtYW5pZmVzdF9zaGEyNTYiOiIxNWMwZTc2ZGMyZTMyMWEzODRlNTNlYzE1ZmY1M2U3NTU1N2I3Y2RlNzFjNWFkMmJhMmI5M2ZmMmU5NTgzNWE1IiwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWF1dGhvcml6YXRpb24tZXZpZGVuY2UvdjEifSwiZWZmZWN0Ijp7ImNvbmZpcm1lZCI6MCwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJtYW5pZmVzdF9zaGEyNTYiOiI3ZTEyZGNhNWM0NGNmOTk4YTQ2OTM2MmI4NjJlODE2YTdkMmM4NTQxNjNmNjNlOGRlNjFmYjc5ZTk1YmE4MDA3IiwicmVjb3JkcyI6W10sInJlcXVpcmVkIjowLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWVmZmVjdC1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiaWRlbnRpdHkiOnsiY29uZmlybWVkIjowLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiZDNhN2VhYzM0MjdkNzZmNmJjMzYwMzA3Mzg2MDkxMTlmMDdlMGQ5MWE2ZDM1OGFiY2U1YjZmMGQ2ZGY3ODY0NSIsInJlcXVpcmVkIjowLCJyZXN1bHRzIjpbXSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1pZGVudGl0eS1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwicG9saWN5Ijp7ImFkbWlzc2lvbl9wb2xpY3lfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImVmZmVjdF9jb250cmFjdF9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwiZW52aXJvbm1lbnRfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImVudmlyb25tZW50X2RpZ2VzdCI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJnb3Zlcm5lZF9wb2xpY3lfY29udHJhY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImdvdmVybmVkX3J1bnRpbWVfaW5wdXRzX2RpZ2VzdCI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibWFuaWZlc3Rfc2hhMjU2IjoiNGY5ODhkMDE1YmNkN2NkN2UwNWJhMDY2M2Q4OWYwYTk2OGYwZDhhY2U5OWIwOTBlYmFiZWU1MmExYzU1MDQyMSIsIm1pbmltdW1fZWZmZWN0X3RpZXIiOjEsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9saWN5LWV2aWRlbmNlL3YxIn0sInBvc3Rjb25kaXRpb24iOnsiY29uZmlybWVkIjowLCJtYW5pZmVzdF9zaGEyNTYiOiI1MmYwOWQ1ZDYzOWIxYzM4M2YzNzc5NjNlNDg3Y2I4OWQ4OTQxNzg1N2U0NGRhN2UzZjIzZTU1YmU3NTFlN2Y0IiwicmVjb3JkcyI6W10sInJlcXVpcmVkIjowLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXBvc3Rjb25kaXRpb24tZXZpZGVuY2UvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn19LCJldmlkZW5jZV9ydW5uZXJfc2lnbmVyX3NoYTI1NiI6IjU2NDc1YWE3NTQ2MzQ3NGMwMjg1ZGY1ZGJmMmJjYWI3M2RhNjUxMzU4ODM5ZTliNzc0ODFiMmVhYjEwNzcwOGMiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImV4ZWN1dGlvbl9vdXRjb21lIjp7ImNvbXBlbnNhdGlvbl9hY3Rpb25zIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iXSwiZXhlY3V0aW9uX2NvbXBsZXRlZCI6ZmFsc2UsImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJvYnNlcnZlZCIsIm1hbmFnZWRfcmVzdWx0X2xvc3NfZXZpZGVuY2Vfc2hhMjU2IjoiMmNkMDc2NDkwY2E1ZTVlOGQ5ZDBiZDE0NmQ1NTljMDZlZDQxM2E2N2Q2OTQ4MWExYWZjYzM1NDQ2NGVjZGIwNyIsIm1vZGVsX2NhbGxzIjowLCJvdXRjb21lIjoiSEFMVEVEIiwicGFzc2VkX2NvbnRyYWN0cyI6eyJhdXRob3JpemF0aW9uIjoxLCJlZmZlY3QiOjAsImlkZW50aXR5IjowLCJwb3N0Y29uZGl0aW9uIjowfSwicG9zdGNvbmRpdGlvbl9ldmlkZW5jZSI6W10sInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJxdWFsaWZpY2F0aW9uX2V2aWRlbmNlX29ubHkiOmZhbHNlLCJyZXF1aXJlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjowLCJpZGVudGl0eSI6MCwicG9zdGNvbmRpdGlvbiI6MH0sInZlcnNpb24iOiJvcGVuYWRhcHQuZXhlY3V0aW9uLW91dGNvbWUvdjEiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIn0sImV4ZWN1dGlvbl9vdXRjb21lX3NoYTI1NiI6IjRmNzdkMTRkNjdjZjQ1OTRmMjE2MjNhZWUxMTUzMTg4Mzk1ODYyNjVmZjFkY2FiNzI3N2Q3NmZlMTU5NDIyMDMiLCJleGVjdXRpb25fcHVycG9zZSI6InByb2R1Y3Rpb24iLCJmaW5hbF9hdXRob3JpdHlfc2VxdWVuY2UiOjAsImZpbmFsX3J1bnRpbWVfZGVsaXZlcnlfc2VxdWVuY2UiOjAsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpZGVudGl0eV9jb250cmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowM1oiLCJtYW5hZ2VkX3Jlc3VsdF9sb3NzIjp7ImFjdHVhdGlvbl9yZXBsYXlfYXV0aG9yaXplZCI6ZmFsc2UsImF1dGhlbnRpY2F0ZWRfcnVubmVyX2lkX3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJhdXRoZW50aWNhdGVkX3Nlc3Npb25faWRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImJsaW5kX3JldHJ5X2F1dGhvcml6ZWQiOmZhbHNlLCJjaGlsZF9yZXBvcnRfcmV0YWluZWQiOmZhbHNlLCJjaGlsZF9zdGFydF9ldmlkZW5jZV9zaGEyNTYiOiJlYzY5MDc5MTg4NmFlMmNlMDBjNGU1MTE1NGMzZjEyMGVhNDQxOWVkMjY1MWUwNzNkYjU3MjQ5MThhM2IwOGM2IiwiY2hpbGRfc3RhcnRlZCI6dHJ1ZSwiY2hpbGRfc3RhcnRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiZGVsaXZlcnlfcmVzdWx0X2xvc3NfY2xvc3VyZV9hcnRpZmFjdF9zaGEyNTYiOiI2NWQwZDgyZDU3N2E1MzIzMDIyYjJhZmNlMWEyNmEyMTZhNTJkMjFlZjk4MmUwNTQwYzI2NmYzMDYzZmJjNzk3IiwiZGVsaXZlcnlfc3RhdGUiOiJDTE9TRURfVU5SRVNPTFZFRF9SRVNVTFRfTE9TUyIsImRpc3BhdGNoX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA5IiwiZGlzcGF0Y2hfc2Vzc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsImVmZmVjdF9hYnNlbmNlX2NsYWltZWQiOmZhbHNlLCJldmlkZW5jZV9zaGEyNTYiOiIyY2QwNzY0OTBjYTVlNWU4ZDlkMGJkMTQ2ZDU1OWMwNmVkNDEzYTY3ZDY5NDgxYTFhZmNjMzU0NDY0ZWNkYjA3IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2lnbmVyX3NoYTI1NiI6IjI0ZjZlZDZhY2JmZTEwMDljMDMwZDdjYTU2N2MzM2NhNDgzMDkxMTQ5ODIzNmI1NTYxYTZjODJhYmVjNWRlMjgiLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbXBvdGVuY3lfa2V5X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJsb3NzX2NvZGUiOiJyZXBvcnRfbWlzc2luZyIsIm1hbmFnZWRfZGlzcGF0Y2hfYmluZGluZ19zaGEyNTYiOiJzaGEyNTY6Y2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsIm5vdF9yZWNlaXZlZF9jbGFpbWVkIjpmYWxzZSwib2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInBlbmRpbmdfYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsInBlbmRpbmdfcGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJyZXBvcnRfcHJvdmVuYW5jZSI6Im91dGVyX3J1bm5lcl9yZWNvdmVyeSIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW5fc3RvcmVfaWRlbnRpdHlfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0Lm1hbmFnZWQtcmVzdWx0LWxvc3MvdjEifSwicGVuZGluZ19wZXJtaXRfY291bnQiOjEsInBlcm1pdF9jaGFpbiI6eyJlbnRyaWVzIjpbXSwicGVuZGluZyI6eyJhY3R1YXRpb25fcmVwbGF5X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0IjpudWxsLCJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0X3NoYTI1NiI6bnVsbCwiZGVsaXZlcnlfc3RhdGUiOiJVTlJFU09MVkVEIiwicGVybWl0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJwZXJtaXRfaWQiOiJwZXJtaXQ6MSIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2NoZWNrZWRfYXQiOiIyMDI2LTA4LTE4VDExOjU5OjMwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2V4cGlyZXNfYXQiOiIyMDI2LTA4LTIwVDExOjAwOjAwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiJjODdmNTc1MWFhOWIyNzljY2UyM2FhMTc4YWZmOTU2ZTE5ZjY5MGZjZDJmOWY3MGUwZDk5OTc4MmE2OTBkNmI4Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiIwQnlIZ3FEeFFacm5nTElGLTZ0Wk9xOWxYYW1jQVFDMGRvVVRuZUtOUGVTLVN5b09iUXlPa0F1UEZLNVBZY056T2JaeGdzZ0FMZFdhSV9zYVVDcGxCZyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwicGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJyZWNlaXB0X2Fic2VuY2Vfb2JzZXJ2ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVuZGluZy1wZXJtaXQvdjIifSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjU5YjM5OTcxZTMwNTEwOWM1NDI5OTQ1YTVmYWI0NjYwYjA5ZjQxZTdhYzNmZmQ5MmUzNTA2YWYwMDMzN2RhOTUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi92MiJ9LCJwZXJtaXRfY291bnQiOjEsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlY2VpcHQiOnsiYXV0aG9yaXphdGlvbl9jb25maXJtZWQiOjEsImF1dGhvcml6YXRpb25fcmVxdWlyZWQiOjEsImJ1bmRsZV9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZHVyYXRpb25fbXMiOjAsImVmZmVjdF90aWVyX3JlYWNoZWQiOiJub25lIiwiZWZmZWN0c19jb25maXJtZWQiOjAsImVmZmVjdHNfcmVxdWlyZWQiOjAsImVzdF9jb3N0X21pY3JvdXNkIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iXSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im9ic2VydmVkIiwiZ2VuZXJhdGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJoZWFscyI6MCwiaWRlbnRpdHlfYXBwbGljYWJsZSI6MCwiaWRlbnRpdHlfYXJtZWQiOjAsImlkZW50aXR5X2NvbmZpcm1lZCI6MCwiaWRlbnRpdHlfcmVxdWlyZWQiOjAsIm1hbmFnZWRfcmVzdWx0X2xvc3NfZXZpZGVuY2Vfc2hhMjU2IjoiMmNkMDc2NDkwY2E1ZTVlOGQ5ZDBiZDE0NmQ1NTljMDZlZDQxM2E2N2Q2OTQ4MWExYWZjYzM1NDQ2NGVjZGIwNyIsIm1vZGVsX2NhbGxzIjowLCJvdXRjb21lIjoiSEFMVEVEIiwib3Zlcl9oYWx0X2NvdW50IjowLCJwb3N0Y29uZGl0aW9uc19jb25maXJtZWQiOjAsInBvc3Rjb25kaXRpb25zX3JlcXVpcmVkIjowLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjpmYWxzZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicHJvdmVuYW5jZSI6InByb2R1Y3Rpb24iLCJyZWNlaXB0X2J1aWxkZXJfdmVyc2lvbiI6IjEuMi4zIiwicnVuZ19oaXN0b2dyYW0iOnt9LCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXJ1bi1yZWNlaXB0L3YxIiwic291cmNlX3JlY2VpcHRfZGlnZXN0IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9yZWNlaXB0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzb3VyY2Vfc2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucnVuLXJlcG9ydC92MSIsInN0ZXBzX29rIjowLCJzdGVwc190b3RhbCI6MSwic3Vic3RyYXRlIjoid2ViIiwidHJhbnNhY3Rpb25fb3V0Y29tZSI6IlJFQ09OQ0lMSUFUSU9OX1JFUVVJUkVEIn0sInJ1bl9yZWNlaXB0X3NoYTI1NiI6IjMwMDllZTJlMGRlMmIzZmM0MDZkNDkxMWFhY2U1OTY0MjBhZWFhZDgyZjZhOGE1MmVlY2MyNjQ4MDAzZTI0NTkiLCJydW5fcmVwb3J0X29iamVjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVuX3JlcG9ydF9vYmplY3RfdmVyc2lvbiI6InZlcnNpb246bWFuYWdlZC1yZXN1bHQtbG9zczoxIiwicnVuX3JlcG9ydF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwicnVudGltZV9zdWJzdHJhdGUiOiJ3ZWIiLCJydW50aW1lX3ZhbGlkYXRpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDUiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLXRlcm1pbmFsLXZlcmlmaWNhdGlvbi92MiIsInRlbmFudF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMiIsInRlcm1pbmFsX3NlcXVlbmNlIjoxLCJ2ZXJpZmllZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIndvcmtmbG93X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAzIiwid29ya2Zsb3dfdmVyc2lvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNCJ9LCJzaWduYXR1cmUiOiJzVlhzVE5yZDdrTGZTdkFjU2Z6TUh1eWtBLThBbHdJUGMtRk5tYkRPVVdsb3BQaC11MGowR0xlX1JxWURQSThFd003MEp4LTJWa0JNdW9BeXhJaGdEZyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZXZpZGVuY2UtcnVubmVyLWVkMjU1MTktNTY0NzVhYTc1NDYzNDc0YyIsInB1YmxpY19rZXkiOiJBNkVIdi9QT0VMNGRjTjBZNTB2QW1XZmsxakNicFExZkhkeUdaQkpWTWJnPSJ9fQ==", "managed_result_loss": { "actuation_replay_authorized": false, "authenticated_runner_id_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", @@ -192,9 +207,9 @@ "schema_version": "openadapt.managed-result-loss/v1" }, "name": "reconciliation-required-managed-result-loss", - "payload_canonical_sha256": "955bae00518f0d07965a05727654e108b618818fd6933f6de2ed8c3d425f27fd", - "signature": "uGtKXVcjqKKrLHO2n8thfaGll99lzExqt2W2PzRfT28onuzs699Tlm05dK9BU9Bd-zX374BCr0mVKHxrvZtlBQ", - "terminal_verification_artifact_sha256": "c2a7ef78aebeb55c585386b7b78e16ac3c84f8d19656d0dd1cb76b65141b8d47" + "payload_canonical_sha256": "c1c4558b3240938ee687ebfc5e2702c3fafd3a34a10cadfad08c76465010da5a", + "signature": "sVXsTNrd7kLfSvAcSfzMHuykA-8AlwIPc-FNmbDOUWlopPh-u0j0GLe_RqYDPI8EwM70Jx-2VkBMuoAyxIhgDg", + "terminal_verification_artifact_sha256": "b61dbd7c53e609c28181a9f31b067eb8c829b37c14b2e99d6ff711fe9c64dcac" }, { "callback": { @@ -207,7 +222,7 @@ "uncertain_delivery": false }, "effect_state": null, - "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjEsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlIjp7InBheWxvYWQiOnsiYWNrbm93bGVkZ2VkX3Blcm1pdF9jb3VudCI6MSwiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6ImVjNjkwNzkxODg2YWUyY2UwMGM0ZTUxMTU0YzNmMTIwZWE0NDE5ZWQyNjUxZTA3M2RiNTcyNDkxOGEzYjA4YzYiLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJjbG9zZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsImNsb3N1cmVfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTEiLCJjbG9zdXJlX3JlcXVlc3Rfc2hhMjU2IjoiZDkzMGFiMWFiYzJiMmEwNGI4NDhiOTU1ZmZkZmZmYTFmYmUwNjU1YTU1MzI1ZTA0ODlhZTExZWQ4NDIwMjI2MCIsImNsb3N1cmVfc2VxdWVuY2UiOjEsImRlbGl2ZXJ5X2Fja25vd2xlZGdtZW50X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9pbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbXBvdGVuY3lfa2V5X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJuZXdfcGVybWl0X2F1dGhvcml6ZWQiOmZhbHNlLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsInBlbmRpbmdfYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjpudWxsLCJwZW5kaW5nX3Blcm1pdF9hcnRpZmFjdF9zaGEyNTYiOm51bGwsInBlbmRpbmdfcGVybWl0X2NvdW50IjowLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInBlcm1pdF9jb3VudCI6MSwicmVjZWlwdF9hYnNlbmNlX29ic2VydmVkX2F0IjpudWxsLCJyZXN1bHRfbG9zc19vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bl9zdG9yZV9pZGVudGl0eV9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZXN1bHQtbG9zcy1jbG9zdXJlLXBheWxvYWQvdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9jYWxsYmFja19yZXF1aXJlZCI6dHJ1ZX0sInBheWxvYWRfc2hhMjU2IjoiNmI3NWY4YjQ1YTdkM2U5Y2ZlODU2NGZmZTMyZjI5YzJiNTk3YzI4MTM2YTk3YTRmNTQ1M2ZlMmFmZDE3MjhhYSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6Imt6dVIycXJjR1Q0cG82LXl6ZFlEZ2gzd2FZamI5V3BRR1VfQm5RTEhUSWJkZDV4QmxyVVNsNkg0Y2VWQzMzcjFCd2JGdFJQYVB2bzA1cGd4Z0U0d0F3Iiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6ImYwY2ExOWE2YjNlYWJlNTk0ZGJjOTA2ODljMmY3NGNiZTVlODJkNGQ5NDdkYjI5YTVkMDdiMTY0MWU4OTYxY2YiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6IjdlMTJkY2E1YzQ0Y2Y5OThhNDY5MzYyYjg2MmU4MTZhN2QyYzg1NDE2M2Y2M2U4ZGU2MWZiNzllOTViYTgwMDciLCJyZWNvcmRzIjpbXSwicmVxdWlyZWQiOjAsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZWZmZWN0LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJpZGVudGl0eSI6eyJjb25maXJtZWQiOjAsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiJkM2E3ZWFjMzQyN2Q3NmY2YmMzNjAzMDczODYwOTExOWYwN2UwZDkxYTZkMzU4YWJjZTViNmYwZDZkZjc4NjQ1IiwicmVxdWlyZWQiOjAsInJlc3VsdHMiOltdLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWlkZW50aXR5LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJwb2xpY3kiOnsiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX3BvbGljeV9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZ292ZXJuZWRfcnVudGltZV9pbnB1dHNfZGlnZXN0IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiI0Zjk4OGQwMTViY2Q3Y2Q3ZTA1YmEwNjYzZDg5ZjBhOTY4ZjBkOGFjZTk5YjA5MGViYWJlZTUyYTFjNTUwNDIxIiwibWluaW11bV9lZmZlY3RfdGllciI6MSwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb2xpY3ktZXZpZGVuY2UvdjEifSwicG9zdGNvbmRpdGlvbiI6eyJjb25maXJtZWQiOjAsIm1hbmlmZXN0X3NoYTI1NiI6IjUyZjA5ZDVkNjM5YjFjMzgzZjM3Nzk2M2U0ODdjYjg5ZDg5NDE3ODU3ZTQ0ZGE3ZTNmMjNlNTViZTc1MWU3ZjQiLCJyZWNvcmRzIjpbXSwicmVxdWlyZWQiOjAsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9zdGNvbmRpdGlvbi1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifX0sImV2aWRlbmNlX3J1bm5lcl9zaWduZXJfc2hhMjU2IjoiNTY0NzVhYTc1NDYzNDc0YzAyODVkZjVkYmYyYmNhYjczZGE2NTEzNTg4MzllOWI3NzQ4MWIyZWFiMTA3NzA4YyIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZXhlY3V0aW9uX291dGNvbWUiOnsiY29tcGVuc2F0aW9uX2FjdGlvbnMiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiJdLCJleGVjdXRpb25fY29tcGxldGVkIjpmYWxzZSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJtYW5hZ2VkX3Jlc3VsdF9sb3NzX2V2aWRlbmNlX3NoYTI1NiI6ImYyMmYyNzA0NGVmMzE1ZTkzOWFiYWRmNmI2NzI1ZmNmMmI5MmYyYTI4MDM5NDA3NTgwZTBmMmY3ZTIzMDU0OTQiLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsInBhc3NlZF9jb250cmFjdHMiOnsiYXV0aG9yaXphdGlvbiI6MSwiZWZmZWN0IjowLCJpZGVudGl0eSI6MCwicG9zdGNvbmRpdGlvbiI6MH0sInBvc3Rjb25kaXRpb25fZXZpZGVuY2UiOltdLCJwcm9kdWN0aW9uX2VsaWdpYmxlIjpmYWxzZSwicHJvZmlsZSI6InN0YW5kYXJkIiwicXVhbGlmaWNhdGlvbl9ldmlkZW5jZV9vbmx5IjpmYWxzZSwicmVxdWlyZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjAsInBvc3Rjb25kaXRpb24iOjB9LCJ2ZXJzaW9uIjoib3BlbmFkYXB0LmV4ZWN1dGlvbi1vdXRjb21lL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJleGVjdXRpb25fb3V0Y29tZV9zaGEyNTYiOiJhOTUxNjFlYjFmOGZjMzBmYjk0ZGNiNzdjZDIzYzc2MjdlM2NhY2EzODBhZmEzNGU0MTE3YzI2NGNmYzhiMmYzIiwiZXhlY3V0aW9uX3B1cnBvc2UiOiJwcm9kdWN0aW9uIiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbnRpdHlfY29udHJhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDNaIiwibWFuYWdlZF9yZXN1bHRfbG9zcyI6eyJhY3R1YXRpb25fcmVwbGF5X2F1dGhvcml6ZWQiOmZhbHNlLCJhdXRoZW50aWNhdGVkX3J1bm5lcl9pZF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiYXV0aGVudGljYXRlZF9zZXNzaW9uX2lkX3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJibGluZF9yZXRyeV9hdXRob3JpemVkIjpmYWxzZSwiY2hpbGRfcmVwb3J0X3JldGFpbmVkIjpmYWxzZSwiY2hpbGRfc3RhcnRfZXZpZGVuY2Vfc2hhMjU2IjoiZWM2OTA3OTE4ODZhZTJjZTAwYzRlNTExNTRjM2YxMjBlYTQ0MTllZDI2NTFlMDczZGI1NzI0OTE4YTNiMDhjNiIsImNoaWxkX3N0YXJ0ZWQiOnRydWUsImNoaWxkX3N0YXJ0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImRlbGl2ZXJ5X3Jlc3VsdF9sb3NzX2Nsb3N1cmVfYXJ0aWZhY3Rfc2hhMjU2IjoiYjJkOTJmMzI0OGY0ZGE2MWVkYjAyN2NmNTlkYTUzOTVmZmM0MThlYWQ2ZDQ4NWMyOWRmNmMzNWJjMjFhOTMwZCIsImRlbGl2ZXJ5X3N0YXRlIjoiQ0xPU0VEX1VOUkVTT0xWRURfUkVTVUxUX0xPU1MiLCJkaXNwYXRjaF9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOSIsImRpc3BhdGNoX3Nlc3Npb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTAiLCJlZmZlY3RfYWJzZW5jZV9jbGFpbWVkIjpmYWxzZSwiZXZpZGVuY2Vfc2hhMjU2IjoiZjIyZjI3MDQ0ZWYzMTVlOTM5YWJhZGY2YjY3MjVmY2YyYjkyZjJhMjgwMzk0MDc1ODBlMGYyZjdlMjMwNTQ5NCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW1wb3RlbmN5X2tleV9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwibG9zc19jb2RlIjoicmVwb3J0X21pc3NpbmciLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsIm9ic2VydmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJwZW5kaW5nX2FjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6bnVsbCwicGVuZGluZ19wZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjpudWxsLCJyZXBvcnRfcHJvdmVuYW5jZSI6Im91dGVyX3J1bm5lcl9yZWNvdmVyeSIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW5fc3RvcmVfaWRlbnRpdHlfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0Lm1hbmFnZWQtcmVzdWx0LWxvc3MvdjEifSwicGVuZGluZ19wZXJtaXRfY291bnQiOjAsInBlcm1pdF9jaGFpbiI6eyJlbnRyaWVzIjpbeyJkZWxpdmVyeV9yZWNlaXB0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiZGVsaXZlcmVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMVoiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4Iiwib25lX3VzZV9jbGFpbV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAxMCIsInBlcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5IiwicGVybWl0X2lkIjoicGVybWl0OjEiLCJydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtcGF5bG9hZC92MiJ9LCJwYXlsb2FkX3NoYTI1NiI6IjhhOGY2ZDg4NzUwY2JiNmIzYmQ2NjIwOTNhZDI5ZTJiYzIwYzZhYjY5OTFkNzBlNTg4NjkxYzhiM2FiNjYyYjkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXJlY2VpcHQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiJBT0txQ2dTMXMwWGEtaXlFOTZWLXlsUHpRY3YydXZPc3V5Z0h6dnUyUnlWN0ZMc0hGX0xWZnF5eDR1MnB1bWxFbDBZazVZdTcwSlZVa3NacGFVcEREdyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwiZGVsaXZlcnlfcmVjZWlwdF9hcnRpZmFjdF9zaGEyNTYiOiJjMDBhMWJhNzAxNWYzY2JlMDVhNzZhZWFiZmY2MTlkNzkzZTczNzVhMzEwZTE5NWZjYmJkMmQyOTQ4ZjJiMjE0IiwicGVybWl0X2FydGlmYWN0Ijp7InBheWxvYWQiOnsiYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaW5wdXRfZWRnZV9zZXF1ZW5jZSI6MSwiaXNzdWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJwZXJtaXRfaWQiOiJwZXJtaXQ6MSIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2NoZWNrZWRfYXQiOiIyMDI2LTA4LTE4VDExOjU5OjMwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X2V4cGlyZXNfYXQiOiIyMDI2LTA4LTIwVDExOjAwOjAwWiIsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiJjODdmNTc1MWFhOWIyNzljY2UyM2FhMTc4YWZmOTU2ZTE5ZjY5MGZjZDJmOWY3MGUwZDk5OTc4MmE2OTBkNmI4Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtYXJ0aWZhY3QvdjIiLCJzaWduYXR1cmUiOiIwQnlIZ3FEeFFacm5nTElGLTZ0Wk9xOWxYYW1jQVFDMGRvVVRuZUtOUGVTLVN5b09iUXlPa0F1UEZLNVBZY056T2JaeGdzZ0FMZFdhSV9zYVVDcGxCZyIsInNpZ25lciI6eyJhbGdvcml0aG0iOiJlZDI1NTE5Iiwia2V5X2lkIjoiZGVsaXZlcnktYXV0aG9yaXR5LWVkMjU1MTktMjRmNmVkNmFjYmZlMTAwOSIsInB1YmxpY19rZXkiOiJLYXk2NFVHOHl2Q3lMaHFVMDAwTHh6WWVVbTBML2hMSWw1UzhreUtXYmRjPSJ9fSwicGVybWl0X2FydGlmYWN0X3NoYTI1NiI6Ijk3MmE4MmFmNDU2ZGE1NDJiYWM3ODQ2MDE4ODk2Njg4OTFkOGI3Y2NkMjQyOTA0ODE2YjMyOTJlMTU3ZTUyODkiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi1lbnRyeS92MiJ9XSwicGVybWl0X2NoYWluX3NoYTI1NiI6IjUwNDhmZTk0NTg4MmQyODAyNjAwOGQ3ZTkxMzdhZDAxNmZkZDAxYWVhZjU4ODQ4MWZiNWJkMGYyNDJhNTAwNDMiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1jaGFpbi92MiJ9LCJwZXJtaXRfY291bnQiOjEsInF1YWxpZmljYXRpb25fc2lnbmVyX3JlZ2lzdHJ5X3JldmlzaW9uIjo3LCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9zaGEyNTYiOiJlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlY2VpcHQiOnsiYXV0aG9yaXphdGlvbl9jb25maXJtZWQiOjEsImF1dGhvcml6YXRpb25fcmVxdWlyZWQiOjEsImJ1bmRsZV9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZHVyYXRpb25fbXMiOjAsImVmZmVjdF90aWVyX3JlYWNoZWQiOiJub25lIiwiZWZmZWN0c19jb25maXJtZWQiOjAsImVmZmVjdHNfcmVxdWlyZWQiOjAsImVzdF9jb3N0X21pY3JvdXNkIjowLCJldmlkZW5jZV9jbGFzc2VzIjpbImF1dGhvcml6YXRpb24iXSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im5vbmUiLCJnZW5lcmF0ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAwWiIsImhlYWxzIjowLCJpZGVudGl0eV9hcHBsaWNhYmxlIjowLCJpZGVudGl0eV9hcm1lZCI6MCwiaWRlbnRpdHlfY29uZmlybWVkIjowLCJpZGVudGl0eV9yZXF1aXJlZCI6MCwibWFuYWdlZF9yZXN1bHRfbG9zc19ldmlkZW5jZV9zaGEyNTYiOiJmMjJmMjcwNDRlZjMxNWU5MzlhYmFkZjZiNjcyNWZjZjJiOTJmMmEyODAzOTQwNzU4MGUwZjJmN2UyMzA1NDk0IiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJvdmVyX2hhbHRfY291bnQiOjAsInBvc3Rjb25kaXRpb25zX2NvbmZpcm1lZCI6MCwicG9zdGNvbmRpdGlvbnNfcmVxdWlyZWQiOjAsInByb2R1Y3Rpb25fZWxpZ2libGUiOmZhbHNlLCJwcm9maWxlIjoic3RhbmRhcmQiLCJwcm92ZW5hbmNlIjoicHJvZHVjdGlvbiIsInJlY2VpcHRfYnVpbGRlcl92ZXJzaW9uIjoiMS4yLjMiLCJydW5nX2hpc3RvZ3JhbSI6e30sInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcnVuLXJlY2VpcHQvdjEiLCJzb3VyY2VfcmVjZWlwdF9kaWdlc3QiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3JlY2VpcHRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInNvdXJjZV9zY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5ydW4tcmVwb3J0L3YxIiwic3RlcHNfb2siOjAsInN0ZXBzX3RvdGFsIjoxLCJzdWJzdHJhdGUiOiJ3ZWIiLCJ0cmFuc2FjdGlvbl9vdXRjb21lIjoiUkVDT05DSUxJQVRJT05fUkVRVUlSRUQifSwicnVuX3JlY2VpcHRfc2hhMjU2IjoiMzI5NzEzZDkwZTA1YjBmNTVlNmFhNTM2NWJmZThiMzA2NGY1ZTQ3OTBlMWNkYmRjOThlMzYyM2Y4N2NlYzVjYyIsInJ1bl9yZXBvcnRfb2JqZWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW5fcmVwb3J0X29iamVjdF92ZXJzaW9uIjoidmVyc2lvbjptYW5hZ2VkLXJlc3VsdC1sb3NzOjEiLCJydW5fcmVwb3J0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJydW50aW1lX2Vudmlyb25tZW50X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJydW50aW1lX3N1YnN0cmF0ZSI6IndlYiIsInJ1bnRpbWVfdmFsaWRhdGlvbl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwNSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tdGVybWluYWwtdmVyaWZpY2F0aW9uL3YyIiwidGVuYW50X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAyIiwidGVybWluYWxfc2VxdWVuY2UiOjEsInZlcmlmaWVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMloiLCJ3b3JrZmxvd19jb250cmFjdF9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwid29ya2Zsb3dfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDMiLCJ3b3JrZmxvd192ZXJzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA0In0sInNpZ25hdHVyZSI6IlgwcWtfTkJXVFVKVk1LQ2NFaFJHcWNJeDRjOTdYd1U2Szg4T0plbHdxc2FpWGVtTV9nRlJ4MHlNYjdPcjhfZUlSNmpMNDNDSVNPMEc0Ums3SWVYNUN3Iiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJldmlkZW5jZS1ydW5uZXItZWQyNTUxOS01NjQ3NWFhNzU0NjM0NzRjIiwicHVibGljX2tleSI6IkE2RUh2L1BPRUw0ZGNOMFk1MHZBbVdmazFqQ2JwUTFmSGR5R1pCSlZNYmc9In19", + "envelope_canonical_base64": "eyJwYXlsb2FkIjp7ImFja25vd2xlZGdlZF9wZXJtaXRfY291bnQiOjEsImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYWRtaXR0ZWRfcnVudGltZV9idWlsZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYnVuZGxlX2FydGlmYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJidW5kbGVfY29udGVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiYnVuZGxlX3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlIjp7InBheWxvYWQiOnsiYWNrbm93bGVkZ2VkX3Blcm1pdF9jb3VudCI6MSwiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6ImVjNjkwNzkxODg2YWUyY2UwMGM0ZTUxMTU0YzNmMTIwZWE0NDE5ZWQyNjUxZTA3M2RiNTcyNDkxOGEzYjA4YzYiLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJjbG9zZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsImNsb3N1cmVfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTEiLCJjbG9zdXJlX3JlcXVlc3Rfc2hhMjU2IjoiZDkzMGFiMWFiYzJiMmEwNGI4NDhiOTU1ZmZkZmZmYTFmYmUwNjU1YTU1MzI1ZTA0ODlhZTExZWQ4NDIwMjI2MCIsImNsb3N1cmVfc2VxdWVuY2UiOjEsImRlbGl2ZXJ5X2Fja25vd2xlZGdtZW50X2F1dGhvcml6ZWQiOmZhbHNlLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZmluYWxfYXV0aG9yaXR5X3NlcXVlbmNlIjowLCJmaW5hbF9pbnB1dF9lZGdlX3NlcXVlbmNlIjoxLCJmaW5hbF9ydW50aW1lX2RlbGl2ZXJ5X3NlcXVlbmNlIjo5LCJmbG93X3J1bl9pZF9zaGEyNTYiOiIxMWU1OTRmNDgxOTU4YzEwZTMwMTVkMGJmMDQ0N2EyMmYwNjhhOGE2NDdmNDc1ZGYxNWNlMmM3YWI0YjhmM2YxIiwiaWRlbXBvdGVuY3lfa2V5X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5hZ2VkX2Rpc3BhdGNoX2JpbmRpbmdfc2hhMjU2Ijoic2hhMjU2OmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJuZXdfcGVybWl0X2F1dGhvcml6ZWQiOmZhbHNlLCJub3RfcmVjZWl2ZWRfY2xhaW1lZCI6ZmFsc2UsInBlbmRpbmdfYWN0aW9uX3JlcXVlc3Rfc2hhMjU2IjpudWxsLCJwZW5kaW5nX3Blcm1pdF9hcnRpZmFjdF9zaGEyNTYiOm51bGwsInBlbmRpbmdfcGVybWl0X2NvdW50IjowLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInBlcm1pdF9jb3VudCI6MSwicmVjZWlwdF9hYnNlbmNlX29ic2VydmVkX2F0IjpudWxsLCJyZXN1bHRfbG9zc19vYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicnVuX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicnVuX3JlcXVlc3Rfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bl9zdG9yZV9pZGVudGl0eV9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZXN1bHQtbG9zcy1jbG9zdXJlLXBheWxvYWQvdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9jYWxsYmFja19yZXF1aXJlZCI6dHJ1ZX0sInBheWxvYWRfc2hhMjU2IjoiNmI3NWY4YjQ1YTdkM2U5Y2ZlODU2NGZmZTMyZjI5YzJiNTk3YzI4MTM2YTk3YTRmNTQ1M2ZlMmFmZDE3MjhhYSIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcmVzdWx0LWxvc3MtY2xvc3VyZS1hcnRpZmFjdC92MiIsInNpZ25hdHVyZSI6Imt6dVIycXJjR1Q0cG82LXl6ZFlEZ2gzd2FZamI5V3BRR1VfQm5RTEhUSWJkZDV4QmxyVVNsNkg0Y2VWQzMzcjFCd2JGdFJQYVB2bzA1cGd4Z0U0d0F3Iiwic2lnbmVyIjp7ImFsZ29yaXRobSI6ImVkMjU1MTkiLCJrZXlfaWQiOiJkZWxpdmVyeS1hdXRob3JpdHktZWQyNTUxOS0yNGY2ZWQ2YWNiZmUxMDA5IiwicHVibGljX2tleSI6IktheTY0VUc4eXZDeUxocVUwMDBMeHpZZVVtMEwvaExJbDVTOGt5S1diZGM9In19LCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImVudmlyb25tZW50X2NvbnRyYWN0X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJlbnZpcm9ubWVudF9kaWdlc3QiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXZpZGVuY2VfaWRlbnRpdHlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsImV2aWRlbmNlX21hbmlmZXN0cyI6eyJhdXRob3JpemF0aW9uIjp7ImFkbWlzc2lvbl9hcnRpZmFjdF9zaGEyNTYiOiJkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkIiwiYWRtaXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA2IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsImV4ZWN1dGlvbl9hdXRob3JpdHlfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX2F1dGhvcml6YXRpb25faWRfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsIm1hbmlmZXN0X3NoYTI1NiI6ImYwY2ExOWE2YjNlYWJlNTk0ZGJjOTA2ODljMmY3NGNiZTVlODJkNGQ5NDdkYjI5YTVkMDdiMTY0MWU4OTYxY2YiLCJwZXJtaXRfY2hhaW5fc2hhMjU2IjoiNTA0OGZlOTQ1ODgyZDI4MDI2MDA4ZDdlOTEzN2FkMDE2ZmRkMDFhZWFmNTg4NDgxZmI1YmQwZjI0MmE1MDA0MyIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tYXV0aG9yaXphdGlvbi1ldmlkZW5jZS92MSJ9LCJlZmZlY3QiOnsiY29uZmlybWVkIjowLCJlZmZlY3RfY29udHJhY3Rfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsIm1hbmlmZXN0X3NoYTI1NiI6IjdlMTJkY2E1YzQ0Y2Y5OThhNDY5MzYyYjg2MmU4MTZhN2QyYzg1NDE2M2Y2M2U4ZGU2MWZiNzllOTViYTgwMDciLCJyZWNvcmRzIjpbXSwicmVxdWlyZWQiOjAsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZWZmZWN0LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJpZGVudGl0eSI6eyJjb25maXJtZWQiOjAsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiJkM2E3ZWFjMzQyN2Q3NmY2YmMzNjAzMDczODYwOTExOWYwN2UwZDkxYTZkMzU4YWJjZTViNmYwZDZkZjc4NjQ1IiwicmVxdWlyZWQiOjAsInJlc3VsdHMiOltdLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWlkZW50aXR5LWV2aWRlbmNlL3YxIiwid29ya2Zsb3dfY29udHJhY3Rfc2hhMjU2IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSJ9LCJwb2xpY3kiOnsiYWRtaXNzaW9uX3BvbGljeV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZWZmZWN0X2NvbnRyYWN0X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJlbnZpcm9ubWVudF9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImdvdmVybmVkX3BvbGljeV9jb250cmFjdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwiZ292ZXJuZWRfcnVudGltZV9pbnB1dHNfZGlnZXN0IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJtYW5pZmVzdF9zaGEyNTYiOiI0Zjk4OGQwMTViY2Q3Y2Q3ZTA1YmEwNjYzZDg5ZjBhOTY4ZjBkOGFjZTk5YjA5MGViYWJlZTUyYTFjNTUwNDIxIiwibWluaW11bV9lZmZlY3RfdGllciI6MSwicnVudGltZV9lbnZpcm9ubWVudF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1wb2xpY3ktZXZpZGVuY2UvdjEifSwicG9zdGNvbmRpdGlvbiI6eyJjb25maXJtZWQiOjAsIm1hbmlmZXN0X3NoYTI1NiI6IjUyZjA5ZDVkNjM5YjFjMzgzZjM3Nzk2M2U0ODdjYjg5ZDg5NDE3ODU3ZTQ0ZGE3ZTNmMjNlNTViZTc1MWU3ZjQiLCJyZWNvcmRzIjpbXSwicmVxdWlyZWQiOjAsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tcG9zdGNvbmRpdGlvbi1ldmlkZW5jZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifX0sImV2aWRlbmNlX3J1bm5lcl9zaWduZXJfc2hhMjU2IjoiNTY0NzVhYTc1NDYzNDc0YzAyODVkZjVkYmYyYmNhYjczZGE2NTEzNTg4MzllOWI3NzQ4MWIyZWFiMTA3NzA4YyIsImV4ZWN1dGlvbl9hdXRob3JpdHlfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDgiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJleGVjdXRpb25fYXV0aG9yaXR5X3NpZ25lcl9zaGEyNTYiOiIyNGY2ZWQ2YWNiZmUxMDA5YzAzMGQ3Y2E1NjdjMzNjYTQ4MzA5MTE0OTgyMzZiNTU2MWE2YzgyYWJlYzVkZTI4IiwiZXhlY3V0aW9uX291dGNvbWUiOnsiY29tcGVuc2F0aW9uX2FjdGlvbnMiOjAsImV2aWRlbmNlX2NsYXNzZXMiOlsiYXV0aG9yaXphdGlvbiJdLCJleGVjdXRpb25fY29tcGxldGVkIjpmYWxzZSwiZXh0ZXJuYWxfbmV0d29ya19jYWxscyI6Im9ic2VydmVkIiwibWFuYWdlZF9yZXN1bHRfbG9zc19ldmlkZW5jZV9zaGEyNTYiOiJmMjJmMjcwNDRlZjMxNWU5MzlhYmFkZjZiNjcyNWZjZjJiOTJmMmEyODAzOTQwNzU4MGUwZjJmN2UyMzA1NDk0IiwibW9kZWxfY2FsbHMiOjAsIm91dGNvbWUiOiJIQUxURUQiLCJwYXNzZWRfY29udHJhY3RzIjp7ImF1dGhvcml6YXRpb24iOjEsImVmZmVjdCI6MCwiaWRlbnRpdHkiOjAsInBvc3Rjb25kaXRpb24iOjB9LCJwb3N0Y29uZGl0aW9uX2V2aWRlbmNlIjpbXSwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInF1YWxpZmljYXRpb25fZXZpZGVuY2Vfb25seSI6ZmFsc2UsInJlcXVpcmVkX2NvbnRyYWN0cyI6eyJhdXRob3JpemF0aW9uIjoxLCJlZmZlY3QiOjAsImlkZW50aXR5IjowLCJwb3N0Y29uZGl0aW9uIjowfSwidmVyc2lvbiI6Im9wZW5hZGFwdC5leGVjdXRpb24tb3V0Y29tZS92MSIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiZXhlY3V0aW9uX291dGNvbWVfc2hhMjU2IjoiY2MzMGNiNGJlMzA1YmM1NzI1NzE1OGVjOThkMjUwYzA4MDgyMjAyOGI1ZTZiYTA5OTQ1N2ViYTQ3NTgxY2MzYyIsImV4ZWN1dGlvbl9wdXJwb3NlIjoicHJvZHVjdGlvbiIsImZpbmFsX2F1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZmluYWxfcnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6OSwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlkZW50aXR5X2NvbnRyYWN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJpc3N1ZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAzWiIsIm1hbmFnZWRfcmVzdWx0X2xvc3MiOnsiYWN0dWF0aW9uX3JlcGxheV9hdXRob3JpemVkIjpmYWxzZSwiYXV0aGVudGljYXRlZF9ydW5uZXJfaWRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsImF1dGhlbnRpY2F0ZWRfc2Vzc2lvbl9pZF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwiYmxpbmRfcmV0cnlfYXV0aG9yaXplZCI6ZmFsc2UsImNoaWxkX3JlcG9ydF9yZXRhaW5lZCI6ZmFsc2UsImNoaWxkX3N0YXJ0X2V2aWRlbmNlX3NoYTI1NiI6ImVjNjkwNzkxODg2YWUyY2UwMGM0ZTUxMTU0YzNmMTIwZWE0NDE5ZWQyNjUxZTA3M2RiNTcyNDkxOGEzYjA4YzYiLCJjaGlsZF9zdGFydGVkIjp0cnVlLCJjaGlsZF9zdGFydGVkX2F0IjoiMjAyNi0wOC0xOFQxMjowMDowMFoiLCJkZWxpdmVyeV9yZXN1bHRfbG9zc19jbG9zdXJlX2FydGlmYWN0X3NoYTI1NiI6ImIyZDkyZjMyNDhmNGRhNjFlZGIwMjdjZjU5ZGE1Mzk1ZmZjNDE4ZWFkNmQ0ODVjMjlkZjZjMzViYzIxYTkzMGQiLCJkZWxpdmVyeV9zdGF0ZSI6IkNMT1NFRF9VTlJFU09MVkVEX1JFU1VMVF9MT1NTIiwiZGlzcGF0Y2hfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDkiLCJkaXNwYXRjaF9zZXNzaW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDEwIiwiZWZmZWN0X2Fic2VuY2VfY2xhaW1lZCI6ZmFsc2UsImV2aWRlbmNlX3NoYTI1NiI6ImYyMmYyNzA0NGVmMzE1ZTkzOWFiYWRmNmI2NzI1ZmNmMmI5MmYyYTI4MDM5NDA3NTgwZTBmMmY3ZTIzMDU0OTQiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaWduZXJfc2hhMjU2IjoiMjRmNmVkNmFjYmZlMTAwOWMwMzBkN2NhNTY3YzMzY2E0ODMwOTExNDk4MjM2YjU1NjFhNmM4MmFiZWM1ZGUyOCIsImZsb3dfcnVuX2lkX3NoYTI1NiI6IjExZTU5NGY0ODE5NThjMTBlMzAxNWQwYmYwNDQ3YTIyZjA2OGE4YTY0N2Y0NzVkZjE1Y2UyYzdhYjRiOGYzZjEiLCJpZGVtcG90ZW5jeV9rZXlfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImxvc3NfY29kZSI6InJlcG9ydF9taXNzaW5nIiwibWFuYWdlZF9kaXNwYXRjaF9iaW5kaW5nX3NoYTI1NiI6InNoYTI1NjpjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwibm90X3JlY2VpdmVkX2NsYWltZWQiOmZhbHNlLCJvYnNlcnZlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDJaIiwicGVuZGluZ19hY3Rpb25fcmVxdWVzdF9zaGEyNTYiOm51bGwsInBlbmRpbmdfcGVybWl0X2FydGlmYWN0X3NoYTI1NiI6bnVsbCwicmVwb3J0X3Byb3ZlbmFuY2UiOiJvdXRlcl9ydW5uZXJfcmVjb3ZlcnkiLCJydW5faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEiLCJydW5fcmVxdWVzdF9zaGEyNTYiOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjIiwicnVuX3N0b3JlX2lkZW50aXR5X3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5tYW5hZ2VkLXJlc3VsdC1sb3NzL3YxIn0sInBlbmRpbmdfcGVybWl0X2NvdW50IjowLCJwZXJtaXRfY2hhaW4iOnsiZW50cmllcyI6W3siZGVsaXZlcnlfcmVjZWlwdF9hcnRpZmFjdCI6eyJwYXlsb2FkIjp7ImF1dGhlbnRpY2F0ZWRfcnVubmVyX2lkX3NoYTI1NiI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJhdXRoZW50aWNhdGVkX3Nlc3Npb25faWRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsImRlbGl2ZXJlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDFaIiwiZXhlY3V0aW9uX2F1dGhvcml0eV9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwOCIsIm9uZV91c2VfY2xhaW1faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMTAiLCJwZXJtaXRfYXJ0aWZhY3Rfc2hhMjU2IjoiOTcyYTgyYWY0NTZkYTU0MmJhYzc4NDYwMTg4OTY2ODg5MWQ4YjdjY2QyNDI5MDQ4MTZiMzI5MmUxNTdlNTI4OSIsInBlcm1pdF9pZCI6InBlcm1pdDoxIiwicnVudGltZV9kZWxpdmVyeV9zZXF1ZW5jZSI6OSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZWNlaXB0LXBheWxvYWQvdjIifSwicGF5bG9hZF9zaGEyNTYiOiI4YThmNmQ4ODc1MGNiYjZiM2JkNjYyMDkzYWQyOWUyYmMyMGM2YWI2OTkxZDcwZTU4ODY5MWM4YjNhYjY2MmI5Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1yZWNlaXB0LWFydGlmYWN0L3YyIiwic2lnbmF0dXJlIjoiQU9LcUNnUzFzMFhhLWl5RTk2Vi15bFB6UWN2MnV2T3N1eWdIenZ1MlJ5VjdGTHNIRl9MVmZxeXg0dTJwdW1sRWwwWWs1WXU3MEpWVWtzWnBhVXBERHciLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImRlbGl2ZXJ5LWF1dGhvcml0eS1lZDI1NTE5LTI0ZjZlZDZhY2JmZTEwMDkiLCJwdWJsaWNfa2V5IjoiS2F5NjRVRzh5dkN5TGhxVTAwMEx4elllVW0wTC9oTElsNVM4a3lLV2JkYz0ifX0sImRlbGl2ZXJ5X3JlY2VpcHRfYXJ0aWZhY3Rfc2hhMjU2IjoiYzAwYTFiYTcwMTVmM2NiZTA1YTc2YWVhYmZmNjE5ZDc5M2U3Mzc1YTMxMGUxOTVmY2JiZDJkMjk0OGYyYjIxNCIsInBlcm1pdF9hcnRpZmFjdCI6eyJwYXlsb2FkIjp7ImFjdGlvbl9yZXF1ZXN0X3NoYTI1NiI6ImRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGQiLCJhZG1pc3Npb25fYXJ0aWZhY3Rfc2hhMjU2IjoiZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZCIsImF1dGhvcml0eV9zZXF1ZW5jZSI6MCwiZW52aXJvbm1lbnRfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImV2aWRlbmNlX2lkZW50aXR5X3NoYTI1NiI6ImVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWUiLCJleGVjdXRpb25fYXV0aG9yaXR5X2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA4IiwiZXhlY3V0aW9uX2F1dGhvcml0eV9zaGEyNTYiOiJhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwiZmxvd19ydW5faWRfc2hhMjU2IjoiMTFlNTk0ZjQ4MTk1OGMxMGUzMDE1ZDBiZjA0NDdhMjJmMDY4YThhNjQ3ZjQ3NWRmMTVjZTJjN2FiNGI4ZjNmMSIsImlucHV0X2VkZ2Vfc2VxdWVuY2UiOjEsImlzc3VlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwicGVybWl0X2lkIjoicGVybWl0OjEiLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9jaGVja2VkX2F0IjoiMjAyNi0wOC0xOFQxMTo1OTozMFoiLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9leHBpcmVzX2F0IjoiMjAyNi0wOC0yMFQxMTowMDowMFoiLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9yZXZpc2lvbiI6NywicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZXF1ZXN0X3NoYTI1NiI6ImNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2MiLCJzY2hlbWFfdmVyc2lvbiI6Im9wZW5hZGFwdC5wcm9kdWN0aW9uLWRlbGl2ZXJ5LXBlcm1pdC1wYXlsb2FkL3YyIn0sInBheWxvYWRfc2hhMjU2IjoiYzg3ZjU3NTFhYTliMjc5Y2NlMjNhYTE3OGFmZjk1NmUxOWY2OTBmY2QyZjlmNzBlMGQ5OTk3ODJhNjkwZDZiOCIsInNjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnByb2R1Y3Rpb24tZGVsaXZlcnktcGVybWl0LWFydGlmYWN0L3YyIiwic2lnbmF0dXJlIjoiMEJ5SGdxRHhRWnJuZ0xJRi02dFpPcTlsWGFtY0FRQzBkb1VUbmVLTlBlUy1TeW9PYlF5T2tBdVBGSzVQWWNOek9iWnhnc2dBTGRXYUlfc2FVQ3BsQmciLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImRlbGl2ZXJ5LWF1dGhvcml0eS1lZDI1NTE5LTI0ZjZlZDZhY2JmZTEwMDkiLCJwdWJsaWNfa2V5IjoiS2F5NjRVRzh5dkN5TGhxVTAwMEx4elllVW0wTC9oTElsNVM4a3lLV2JkYz0ifX0sInBlcm1pdF9hcnRpZmFjdF9zaGEyNTYiOiI5NzJhODJhZjQ1NmRhNTQyYmFjNzg0NjAxODg5NjY4ODkxZDhiN2NjZDI0MjkwNDgxNmIzMjkyZTE1N2U1Mjg5Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtY2hhaW4tZW50cnkvdjIifV0sInBlcm1pdF9jaGFpbl9zaGEyNTYiOiI1MDQ4ZmU5NDU4ODJkMjgwMjYwMDhkN2U5MTM3YWQwMTZmZGQwMWFlYWY1ODg0ODFmYjViZDBmMjQyYTUwMDQzIiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1kZWxpdmVyeS1wZXJtaXQtY2hhaW4vdjIifSwicGVybWl0X2NvdW50IjoxLCJxdWFsaWZpY2F0aW9uX3NpZ25lcl9yZWdpc3RyeV9yZXZpc2lvbiI6NywicXVhbGlmaWNhdGlvbl9zaWduZXJfcmVnaXN0cnlfc2hhMjU2IjoiZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZSIsInJ1bl9pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMSIsInJ1bl9yZWNlaXB0Ijp7ImF1dGhvcml6YXRpb25fY29uZmlybWVkIjoxLCJhdXRob3JpemF0aW9uX3JlcXVpcmVkIjoxLCJidW5kbGVfZGlnZXN0IjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYSIsImR1cmF0aW9uX21zIjowLCJlZmZlY3RfdGllcl9yZWFjaGVkIjoibm9uZSIsImVmZmVjdHNfY29uZmlybWVkIjowLCJlZmZlY3RzX3JlcXVpcmVkIjowLCJlc3RfY29zdF9taWNyb3VzZCI6MCwiZXZpZGVuY2VfY2xhc3NlcyI6WyJhdXRob3JpemF0aW9uIl0sImV4dGVybmFsX25ldHdvcmtfY2FsbHMiOiJvYnNlcnZlZCIsImdlbmVyYXRlZF9hdCI6IjIwMjYtMDgtMThUMTI6MDA6MDBaIiwiaGVhbHMiOjAsImlkZW50aXR5X2FwcGxpY2FibGUiOjAsImlkZW50aXR5X2FybWVkIjowLCJpZGVudGl0eV9jb25maXJtZWQiOjAsImlkZW50aXR5X3JlcXVpcmVkIjowLCJtYW5hZ2VkX3Jlc3VsdF9sb3NzX2V2aWRlbmNlX3NoYTI1NiI6ImYyMmYyNzA0NGVmMzE1ZTkzOWFiYWRmNmI2NzI1ZmNmMmI5MmYyYTI4MDM5NDA3NTgwZTBmMmY3ZTIzMDU0OTQiLCJtb2RlbF9jYWxscyI6MCwib3V0Y29tZSI6IkhBTFRFRCIsIm92ZXJfaGFsdF9jb3VudCI6MCwicG9zdGNvbmRpdGlvbnNfY29uZmlybWVkIjowLCJwb3N0Y29uZGl0aW9uc19yZXF1aXJlZCI6MCwicHJvZHVjdGlvbl9lbGlnaWJsZSI6ZmFsc2UsInByb2ZpbGUiOiJzdGFuZGFyZCIsInByb3ZlbmFuY2UiOiJwcm9kdWN0aW9uIiwicmVjZWlwdF9idWlsZGVyX3ZlcnNpb24iOiIxLjIuMyIsInJ1bmdfaGlzdG9ncmFtIjp7fSwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi1ydW4tcmVjZWlwdC92MSIsInNvdXJjZV9yZWNlaXB0X2RpZ2VzdCI6ImJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmIiLCJzb3VyY2VfcmVjZWlwdF9zaGEyNTYiOiJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiIiwic291cmNlX3NjaGVtYV92ZXJzaW9uIjoib3BlbmFkYXB0LnJ1bi1yZXBvcnQvdjEiLCJzdGVwc19vayI6MCwic3RlcHNfdG90YWwiOjEsInN1YnN0cmF0ZSI6IndlYiIsInRyYW5zYWN0aW9uX291dGNvbWUiOiJSRUNPTkNJTElBVElPTl9SRVFVSVJFRCJ9LCJydW5fcmVjZWlwdF9zaGEyNTYiOiJjOWRkNjNkNWY1MDEzNGU1NDZmNzJkMmQ4ZTcyMjJkMGEwNDM1ZWIwMmVkY2M4NWVjYTZjNzY5MjMwNWRjZTMwIiwicnVuX3JlcG9ydF9vYmplY3Rfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bl9yZXBvcnRfb2JqZWN0X3ZlcnNpb24iOiJ2ZXJzaW9uOm1hbmFnZWQtcmVzdWx0LWxvc3M6MSIsInJ1bl9yZXBvcnRfc2hhMjU2IjoiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYiIsInJ1bnRpbWVfZW52aXJvbm1lbnRfc2hhMjU2IjoiY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyIsInJ1bnRpbWVfc3Vic3RyYXRlIjoid2ViIiwicnVudGltZV92YWxpZGF0aW9uX2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDA1Iiwic2NoZW1hX3ZlcnNpb24iOiJvcGVuYWRhcHQucHJvZHVjdGlvbi10ZXJtaW5hbC12ZXJpZmljYXRpb24vdjIiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDIiLCJ0ZXJtaW5hbF9zZXF1ZW5jZSI6MSwidmVyaWZpZWRfYXQiOiIyMDI2LTA4LTE4VDEyOjAwOjAyWiIsIndvcmtmbG93X2NvbnRyYWN0X3NoYTI1NiI6ImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEiLCJ3b3JrZmxvd19pZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDAwMyIsIndvcmtmbG93X3ZlcnNpb25faWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDQifSwic2lnbmF0dXJlIjoiU212VGw5N1MwNm9pb2NNMGNza0tIQ3ZnV1Q5T1dDN3V4NkN4WXljQ2hyMjFjY3B2Y3ExT3R3MUh5V1ZJMGhubTNDRExJcWgtVmN2cTh4TmdPTm40QkEiLCJzaWduZXIiOnsiYWxnb3JpdGhtIjoiZWQyNTUxOSIsImtleV9pZCI6ImV2aWRlbmNlLXJ1bm5lci1lZDI1NTE5LTU2NDc1YWE3NTQ2MzQ3NGMiLCJwdWJsaWNfa2V5IjoiQTZFSHYvUE9FTDRkY04wWTUwdkFtV2ZrMWpDYnBRMWZIZHlHWkJKVk1iZz0ifX0=", "managed_result_loss": { "actuation_replay_authorized": false, "authenticated_runner_id_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", @@ -241,9 +256,9 @@ "schema_version": "openadapt.managed-result-loss/v1" }, "name": "reconciliation-required-managed-result-loss-acknowledged", - "payload_canonical_sha256": "c024765711724b2645dbe23c250d301079e9045cc45f54eba72bacab3a22233b", - "signature": "X0qk_NBWTUJVMKCcEhRGqcIx4c97XwU6K88OJelwqsaiXemM_gFRx0yMb7Or8_eIR6jL43CISO0G4Rk7IeX5Cw", - "terminal_verification_artifact_sha256": "1707d4b13e8549cb7b12edae186866006b0211933830384d9fca6e1fc0b54756" + "payload_canonical_sha256": "5a09dfb147c5ba0cdab0038fb33e9ac497c5b95ccb2b85abd4fc5171f8ca22f1", + "signature": "SmvTl97S06oiocM0cskKHCvgWT9OWC7ux6CxYycChr21ccpvcq1Otw1HyWVI0hnm3CDLIqh-Vcvq8xNgONn4BA", + "terminal_verification_artifact_sha256": "eea2765e1d7f414e8299125eb3187b62c653f320ccaf08f45dc7bf572e2f5ae2" } ] } diff --git a/tests/test_hosted_runner_adapter.py b/tests/test_hosted_runner_adapter.py index 1dae494f..67f2574b 100644 --- a/tests/test_hosted_runner_adapter.py +++ b/tests/test_hosted_runner_adapter.py @@ -1015,8 +1015,17 @@ def runner(argv, _run_dir, child_env): ManagedExecution(returncode=1, report_bytes=None), "report_missing", ), + ( + ManagedExecution(returncode=1, report_bytes=b"{"), + "report_invalid", + ), ], - ids=("runtime-exception", "transport-exception", "missing-report"), + ids=( + "runtime-exception", + "transport-exception", + "missing-report", + "malformed-report", + ), ) def test_expected_result_loss_returns_one_signed_reconciliation_without_retry( monkeypatch, @@ -1065,11 +1074,21 @@ def runner(_argv, _run_dir, _child_env): ) report = _production_report() seen_loss_codes: list[str] = [] + seen_fence_codes: list[str] = [] + + def retain_loss_closure(**kwargs): + seen_fence_codes.append(kwargs["loss_code"]) + return object() def produce_loss(**kwargs): seen_loss_codes.append(kwargs["loss_code"]) return proof, proof.payload.run_report_sha256, report + monkeypatch.setattr( + adapter, + "_retain_managed_result_loss_closure", + retain_loss_closure, + ) monkeypatch.setattr(adapter, "_produce_managed_result_loss", produce_loss) authority = DeliveryAuthority( dispatch.managed_delivery_authority_url, @@ -1086,6 +1105,7 @@ def produce_loss(**kwargs): callback = adapter.callback_request(dispatch, result) assert calls == 1 + assert seen_fence_codes == [expected_loss_code] assert seen_loss_codes == [expected_loss_code] assert result.outcome is TransactionOutcome.RECONCILIATION_REQUIRED assert result.uncertain_delivery is True @@ -1112,6 +1132,177 @@ def produce_loss(**kwargs): assert calls == 1 +@pytest.mark.parametrize( + ("result_mode", "expected_loss_code"), + [ + ("runner-exception", "runner_exception"), + ("missing-report", "report_missing"), + ("malformed-report", "report_invalid"), + ("conflicting-child-proof", "report_invalid"), + ], +) +def test_result_loss_fences_before_terminal_trust_revalidation( + monkeypatch, + tmp_path, + config, + sealed, + result_mode, + expected_loss_code, +) -> None: + workflow, _ = sealed + + def runner(_argv, _run_dir, _child_env): + if result_mode == "runner-exception": + raise RuntimeError("child result lost") + if result_mode == "missing-report": + return ManagedExecution(returncode=1, report_bytes=None) + if result_mode == "malformed-report": + return ManagedExecution(returncode=1, report_bytes=b"{") + return ManagedExecution( + returncode=0, + report_bytes=_production_report().model_dump_json().encode("utf-8"), + terminal_verification=sign_production_terminal_verification( + _managed_result_loss_payload(), + _private_key(), + ), + ) + + adapter, dispatch = _prepared_adapter( + monkeypatch, + tmp_path, + config, + workflow, + runner, + ) + fence_codes: list[str] = [] + + def retain_fence(**kwargs): + fence_codes.append(kwargs["loss_code"]) + return object() + + monkeypatch.setattr( + adapter, + "_retain_managed_result_loss_closure", + retain_fence, + ) + monkeypatch.setattr( + adapter, + "_revalidate_terminal_authority", + lambda **_kwargs: (_ for _ in ()).throw(ValueError("authority revoked")), + ) + authority = DeliveryAuthority( + dispatch.managed_delivery_authority_url, + dispatch.delivery_authority_token, + ) + + with pytest.raises(RuntimeError, match="signed terminal reconciliation"): + adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run", + authority=authority, + closure_authority=SimpleNamespace(), + ) + + assert fence_codes == [expected_loss_code] + reservation = adapter._ledger.lookup( + f"{dispatch.tenant_id}:{dispatch.idempotency_key}" + ) + assert reservation is not None + assert reservation["outcome"] == "RECONCILIATION_REQUIRED" + + +def test_result_loss_fences_before_missing_child_manifest_is_read( + monkeypatch, tmp_path, config, sealed +) -> None: + workflow, _ = sealed + + def runner(_argv, _run_dir, _child_env): + raise RuntimeError("child result lost") + + adapter, dispatch = _prepared_adapter( + monkeypatch, + tmp_path, + config, + workflow, + runner, + ) + fenced = object() + monkeypatch.setattr( + adapter, + "_retain_managed_result_loss_closure", + lambda **_kwargs: fenced, + ) + monkeypatch.setattr( + adapter, + "_revalidate_terminal_authority", + lambda **_kwargs: (object(), object()), + ) + authority = DeliveryAuthority( + dispatch.managed_delivery_authority_url, + dispatch.delivery_authority_token, + ) + + with pytest.raises(RuntimeError, match="signed terminal reconciliation") as error: + adapter.execute( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=tmp_path / "run", + authority=authority, + closure_authority=SimpleNamespace(), + ) + + assert isinstance(error.value.__cause__, ValueError) + assert "retained run manifest" in str(error.value.__cause__) + + +def test_restart_recovery_fences_before_current_runner_trust_is_loaded( + monkeypatch, tmp_path, sealed +) -> None: + workflow, _ = sealed + dispatch = _hosted_dispatch(workflow) + adapter = HostedRunnerAdapter(tmp_path / "ledger.sqlite") + run_dir = tmp_path / "run" + run_dir.mkdir(mode=0o700) + adapter._write_child_start_evidence( + dispatch=dispatch, + run_dir=run_dir, + dispatch_binding_sha256=dispatch.payload.dispatch_binding_sha256, + ) + adapter._ledger.reserve( + f"{dispatch.tenant_id}:{dispatch.idempotency_key}", + run_id=dispatch.run_id, + ) + fence_codes: list[str] = [] + + def retain_fence(**kwargs): + fence_codes.append(kwargs["loss_code"]) + return object() + + monkeypatch.setattr( + adapter, + "_retain_managed_result_loss_closure", + retain_fence, + ) + monkeypatch.setattr( + hosted, + "load_runner_config", + lambda *_args, **_kwargs: (_ for _ in ()).throw( + ValueError("runner trust unavailable") + ), + ) + + with pytest.raises(ValueError, match="runner trust unavailable"): + adapter.recover_uncertain_run( + dispatch, + runner_config=tmp_path / "runner.toml", + run_dir=run_dir, + closure_authority=SimpleNamespace(), + ) + + assert fence_codes == ["recovered_after_restart"] + + def test_managed_result_loss_report_is_outer_recovery_only(tmp_path, sealed) -> None: workflow, _ = sealed dispatch = _hosted_dispatch(workflow) diff --git a/tests/test_terminal_verification_v2.py b/tests/test_terminal_verification_v2.py index 92cc246f..f5813809 100644 --- a/tests/test_terminal_verification_v2.py +++ b/tests/test_terminal_verification_v2.py @@ -944,7 +944,7 @@ def _managed_result_loss_payload( postcondition_evidence=(), evidence_classes=("authorization",), model_calls=0, - external_network_calls="none", + external_network_calls="observed", managed_result_loss_evidence_sha256=loss.evidence_sha256, ) receipt = ProductionRunReceipt( @@ -976,7 +976,7 @@ def _managed_result_loss_payload( substrate="web", provenance="production", receipt_builder_version="1.2.3", - external_network_calls="none", + external_network_calls="observed", bundle_digest=SHA_A, source_receipt_digest=SHA_B, source_receipt_sha256=SHA_B, @@ -1317,6 +1317,25 @@ def test_terminal_v2_signs_managed_result_loss_without_effect_absence() -> None: assert loss.blind_retry_authorized is False assert loss.actuation_replay_authorized is False assert envelope.payload.managed_result_loss == loss + assert payload.execution_outcome.external_network_calls == "observed" + assert payload.run_receipt.external_network_calls == "observed" + + +def test_managed_result_loss_requires_observed_closure_network_io() -> None: + raw = _managed_result_loss_payload().model_dump(mode="json") + outcome = ProductionExecutionOutcome.model_validate(raw["execution_outcome"]) + outcome = outcome.model_copy(update={"external_network_calls": "none"}) + receipt = ProductionRunReceipt.model_validate(raw["run_receipt"]) + receipt = receipt.model_copy(update={"external_network_calls": "none"}) + raw["execution_outcome"] = outcome.model_dump(mode="json") + raw["execution_outcome_sha256"] = outcome.artifact_sha256() + raw["run_receipt"] = receipt.model_dump(mode="json") + raw["run_receipt_sha256"] = hashlib.sha256( + canonical_json(receipt.model_dump(mode="json")) + ).hexdigest() + + with pytest.raises(ValueError, match="managed result loss terminal binding"): + ProductionTerminalVerificationPayload.model_validate(raw) def test_terminal_v2_signs_ack_won_managed_result_loss_without_uncertainty() -> None: @@ -1871,42 +1890,56 @@ def test_managed_result_loss_closure_cross_language_vector_is_exact() -> None: encoding="utf-8" ) ) - vector = fixture["managed_result_loss_closure_vector"] - request_raw = b64decode(vector["request_canonical_base64"], validate=True) - closure_raw = b64decode( - vector["closure_artifact_canonical_base64"], validate=True - ) - chain_raw = b64decode(vector["permit_chain_canonical_base64"], validate=True) - result_raw = b64decode(vector["result_canonical_base64"], validate=True) - request = ProductionDeliveryResultLossClosureRequest.model_validate_json( - request_raw - ) - closure = ProductionDeliveryResultLossClosureArtifact.model_validate_json( - closure_raw + names = ( + "managed_result_loss_closure_vector", + "managed_result_loss_acknowledged_closure_vector", ) - chain = ProductionDeliveryPermitChain.model_validate_json(chain_raw) - result = ProductionDeliveryResultLossClosureResult.model_validate_json(result_raw) + pending_counts: list[int] = [] + for name in names: + vector = fixture[name] + request_raw = b64decode(vector["request_canonical_base64"], validate=True) + closure_raw = b64decode( + vector["closure_artifact_canonical_base64"], validate=True + ) + chain_raw = b64decode( + vector["permit_chain_canonical_base64"], validate=True + ) + result_raw = b64decode(vector["result_canonical_base64"], validate=True) + request = ProductionDeliveryResultLossClosureRequest.model_validate_json( + request_raw + ) + closure = ProductionDeliveryResultLossClosureArtifact.model_validate_json( + closure_raw + ) + chain = ProductionDeliveryPermitChain.model_validate_json(chain_raw) + result = ProductionDeliveryResultLossClosureResult.model_validate_json( + result_raw + ) - assert vector["http_method"] == "POST" - assert vector["http_route"] == ( - "/api/internal/managed-delivery-result-loss-closure" - ) - assert vector["authorization_credential_source"] == "hosted_dispatch.lease_token" - assert b64decode(vector["request_digest_domain_base64"], validate=True) == ( - RESULT_LOSS_CLOSURE_REQUEST_DOMAIN - ) - assert b64decode(vector["payload_signature_domain_base64"], validate=True) == ( - RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN - ) - assert request.canonical_bytes() == request_raw - assert request.request_sha256() == vector["request_sha256"] - assert closure.canonical_bytes() == closure_raw - assert closure.payload_sha256 == vector["closure_payload_sha256"] - assert closure.artifact_sha256() == vector["closure_artifact_sha256"] - assert canonical_json(chain) == chain_raw - assert chain.permit_chain_sha256 == vector["permit_chain_sha256"] - assert canonical_json(result) == result_raw - assert result.artifacts() == (closure, chain) + assert vector["http_method"] == "POST" + assert vector["http_route"] == ( + "/api/internal/managed-delivery-result-loss-closure" + ) + assert vector["authorization_credential_source"] == ( + "hosted_dispatch.lease_token" + ) + assert b64decode(vector["request_digest_domain_base64"], validate=True) == ( + RESULT_LOSS_CLOSURE_REQUEST_DOMAIN + ) + assert b64decode( + vector["payload_signature_domain_base64"], validate=True + ) == (RESULT_LOSS_CLOSURE_PAYLOAD_DOMAIN) + assert request.canonical_bytes() == request_raw + assert request.request_sha256() == vector["request_sha256"] + assert closure.canonical_bytes() == closure_raw + assert closure.payload_sha256 == vector["closure_payload_sha256"] + assert closure.artifact_sha256() == vector["closure_artifact_sha256"] + assert canonical_json(chain) == chain_raw + assert chain.permit_chain_sha256 == vector["permit_chain_sha256"] + assert canonical_json(result) == result_raw + assert result.artifacts() == (closure, chain) + pending_counts.append(closure.payload.pending_permit_count) + assert pending_counts == [1, 0] def test_delivery_artifact_rebuild_rejects_noncanonical_stored_bytes() -> None: