Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Corentin André <corentin.andre@mistral.ai> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com> Co-authored-by: Peter Evers <pevers90@gmail.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: MichisGitIsKing <MichisGitIsKing@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
191 lines
6.3 KiB
Python
191 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Awaitable, Callable
|
|
from dataclasses import dataclass
|
|
from datetime import UTC, datetime, timedelta
|
|
from urllib.parse import urlencode
|
|
|
|
from vibe.setup.auth import (
|
|
BrowserSignInError,
|
|
BrowserSignInErrorCode,
|
|
BrowserSignInGateway,
|
|
BrowserSignInPollResult,
|
|
BrowserSignInProcess,
|
|
)
|
|
from vibe.setup.auth.browser_sign_in import BrowserSignInService
|
|
|
|
|
|
@dataclass
|
|
class ExchangeRequestPayload:
|
|
process_id: str
|
|
exchange_token: str
|
|
code_verifier: str
|
|
|
|
|
|
class StubBrowserSignInGateway(BrowserSignInGateway):
|
|
def __init__(
|
|
self,
|
|
*,
|
|
process: BrowserSignInProcess | None = None,
|
|
processes: list[BrowserSignInProcess] | None = None,
|
|
poll_results: list[BrowserSignInPollResult | BrowserSignInError] | None = None,
|
|
exchange_result: str = "sk-browser-key",
|
|
exchange_error: BrowserSignInError | None = None,
|
|
) -> None:
|
|
if process is not None and processes is not None:
|
|
msg = "StubBrowserSignInGateway accepts either process or processes."
|
|
raise AssertionError(msg)
|
|
|
|
self._processes = list(processes or ([] if process is None else [process]))
|
|
self._poll_results = list(poll_results or [])
|
|
self.exchange_result = exchange_result
|
|
self.exchange_error = exchange_error
|
|
self.code_challenges: list[str] = []
|
|
self.polled_urls: list[str] = []
|
|
self.exchange_requests: list[ExchangeRequestPayload] = []
|
|
self.closed = False
|
|
self.poll_calls = 0
|
|
self.process_number = 0
|
|
|
|
async def create_process(self, code_challenge: str) -> BrowserSignInProcess:
|
|
self.code_challenges.append(code_challenge)
|
|
if not self._processes:
|
|
msg = "StubBrowserSignInGateway requires at least one scripted process."
|
|
raise AssertionError(msg)
|
|
|
|
self.process_number += 1
|
|
return self._processes.pop(0)
|
|
|
|
async def poll(self, poll_url: str) -> BrowserSignInPollResult:
|
|
self.polled_urls.append(poll_url)
|
|
self.poll_calls += 1
|
|
if not self._poll_results:
|
|
msg = "StubBrowserSignInGateway requires scripted poll results."
|
|
raise AssertionError(msg)
|
|
result = self._poll_results.pop(0)
|
|
if isinstance(result, BrowserSignInError):
|
|
raise result
|
|
return result
|
|
|
|
async def exchange(
|
|
self, process_id: str, exchange_token: str, code_verifier: str
|
|
) -> str:
|
|
self.exchange_requests.append(
|
|
ExchangeRequestPayload(
|
|
process_id=process_id,
|
|
exchange_token=exchange_token,
|
|
code_verifier=code_verifier,
|
|
)
|
|
)
|
|
if self.exchange_error is not None:
|
|
raise self.exchange_error
|
|
return self.exchange_result
|
|
|
|
async def aclose(self) -> None:
|
|
self.closed = True
|
|
|
|
|
|
def build_sign_in_process(
|
|
now: datetime, process_id: str = "process-1"
|
|
) -> BrowserSignInProcess:
|
|
fragment = urlencode({
|
|
"process_id": process_id,
|
|
"complete_token": f"complete-token-{process_id}",
|
|
"state": f"state-{process_id}",
|
|
})
|
|
return BrowserSignInProcess(
|
|
process_id=process_id,
|
|
sign_in_url=(
|
|
f"https://console.mistral.ai/codestral/cli/authenticate#{fragment}"
|
|
),
|
|
poll_url=(
|
|
f"https://api.mistral.ai/api/vibe/sign-in/poll/poll-token-{process_id}"
|
|
),
|
|
expires_at=now + timedelta(minutes=5),
|
|
)
|
|
|
|
|
|
def build_poll_failed_error() -> BrowserSignInError:
|
|
return BrowserSignInError(
|
|
"Browser sign-in status could not be retrieved.",
|
|
code=BrowserSignInErrorCode.POLL_FAILED,
|
|
)
|
|
|
|
|
|
def build_poll_results_from_outcomes(
|
|
outcomes: list[str],
|
|
) -> tuple[
|
|
list[BrowserSignInProcess], list[BrowserSignInPollResult | BrowserSignInError]
|
|
]:
|
|
processes: list[BrowserSignInProcess] = []
|
|
poll_results: list[BrowserSignInPollResult | BrowserSignInError] = []
|
|
now = datetime(2026, 3, 16, tzinfo=UTC)
|
|
|
|
for process_index, outcome in enumerate(outcomes, start=1):
|
|
process_id = f"process-{process_index}"
|
|
processes.append(build_sign_in_process(now, process_id=process_id))
|
|
|
|
match outcome:
|
|
case "completed":
|
|
poll_results.extend([
|
|
BrowserSignInPollResult(status="pending"),
|
|
BrowserSignInPollResult(
|
|
status="completed", exchange_token=f"exchange-{process_id}"
|
|
),
|
|
])
|
|
case "expired":
|
|
poll_results.append(BrowserSignInPollResult(status="expired"))
|
|
case "poll_failed":
|
|
poll_results.extend([
|
|
BrowserSignInPollResult(status="pending"),
|
|
build_poll_failed_error(),
|
|
build_poll_failed_error(),
|
|
build_poll_failed_error(),
|
|
])
|
|
case _:
|
|
msg = f"Unsupported browser sign-in outcome: {outcome}"
|
|
raise AssertionError(msg)
|
|
|
|
return processes, poll_results
|
|
|
|
|
|
async def noop_sleep(_: float) -> None:
|
|
return None
|
|
|
|
|
|
def build_browser_sign_in_service_factory(
|
|
outcomes: list[str],
|
|
*,
|
|
exchange_result: str = "sk-browser-onboarding-test-key",
|
|
open_browser: Callable[[str], bool] | None = None,
|
|
sleep: Callable[[float], Awaitable[None]] = noop_sleep,
|
|
now: Callable[[], datetime] | None = None,
|
|
) -> tuple[
|
|
StubBrowserSignInGateway,
|
|
Callable[[], BrowserSignInService],
|
|
list[BrowserSignInService],
|
|
]:
|
|
processes, poll_results = build_poll_results_from_outcomes(outcomes)
|
|
gateway = StubBrowserSignInGateway(
|
|
processes=processes, poll_results=poll_results, exchange_result=exchange_result
|
|
)
|
|
created_services: list[BrowserSignInService] = []
|
|
|
|
def build_service() -> BrowserSignInService:
|
|
service = BrowserSignInService(
|
|
gateway,
|
|
open_browser=open_browser or (lambda _: True),
|
|
sleep=sleep,
|
|
now=now
|
|
or (
|
|
lambda: (
|
|
datetime(2026, 3, 16, tzinfo=UTC)
|
|
+ timedelta(seconds=gateway.poll_calls)
|
|
)
|
|
),
|
|
poll_interval=0,
|
|
)
|
|
created_services.append(service)
|
|
return service
|
|
|
|
return gateway, build_service, created_services
|