v2.10.1 (#702)
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
228f3c65a9
commit
f71bfd3b8c
57 changed files with 2950 additions and 402 deletions
|
|
@ -99,7 +99,7 @@ def build_sign_in_process(
|
|||
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}"
|
||||
f"https://console.mistral.ai/api/vibe/sign-in/poll/poll-token-{process_id}"
|
||||
),
|
||||
expires_at=now + timedelta(minutes=5),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ TEST_SIGN_IN_URL = "https://console.mistral.ai/codestral/cli/authenticate#" + ur
|
|||
"complete_token": f"complete-token-{TEST_PROCESS_ID}",
|
||||
"state": f"state-{TEST_PROCESS_ID}",
|
||||
})
|
||||
TEST_POLL_URL = "https://api.mistral.ai/api/vibe/sign-in/poll/poll-token-process-1"
|
||||
TEST_POLL_URL = "https://console.mistral.ai/api/vibe/sign-in/poll/poll-token-process-1"
|
||||
|
||||
|
||||
def build_code_challenge(verifier: str) -> str:
|
||||
|
|
@ -94,6 +94,50 @@ async def test_authenticate_returns_api_key_after_pending_poll() -> None:
|
|||
assert gateway.exchange_requests[0].exchange_token == "exchange-1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_attempt_returns_attempt_without_opening_browser() -> None:
|
||||
opened_urls: list[str] = []
|
||||
gateway, service = build_test_service(
|
||||
poll_results=[], open_browser=lambda url: opened_urls.append(url) or True
|
||||
)
|
||||
|
||||
attempt = await service.start_attempt()
|
||||
|
||||
assert opened_urls == []
|
||||
assert attempt.process_id == TEST_PROCESS_ID
|
||||
assert attempt.sign_in_url == TEST_SIGN_IN_URL
|
||||
assert attempt.poll_url == TEST_POLL_URL
|
||||
assert attempt.expires_at == build_sign_in_process(TEST_NOW).expires_at
|
||||
assert gateway.exchange_requests == []
|
||||
assert gateway.code_challenges == [build_code_challenge(attempt.code_verifier)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_complete_attempt_returns_api_key_without_opening_browser() -> None:
|
||||
opened_urls: list[str] = []
|
||||
statuses: list[BrowserSignInStatus] = []
|
||||
gateway, service = build_test_service(
|
||||
poll_results=[
|
||||
BrowserSignInPollResult(status="pending"),
|
||||
BrowserSignInPollResult(status="completed", exchange_token="exchange-1"),
|
||||
],
|
||||
open_browser=lambda url: opened_urls.append(url) or True,
|
||||
)
|
||||
attempt = await service.start_attempt()
|
||||
|
||||
api_key = await service.complete_attempt(attempt, status_callback=statuses.append)
|
||||
|
||||
assert api_key == "sk-browser-key"
|
||||
assert opened_urls == []
|
||||
assert statuses == [
|
||||
BrowserSignInStatus.WAITING_FOR_BROWSER_SIGN_IN,
|
||||
BrowserSignInStatus.EXCHANGING,
|
||||
BrowserSignInStatus.COMPLETED,
|
||||
]
|
||||
assert gateway.polled_urls == [TEST_POLL_URL, TEST_POLL_URL]
|
||||
assert gateway.exchange_requests[0].exchange_token == "exchange-1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_raises_when_polling_expires() -> None:
|
||||
opened_urls: list[str] = []
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ from vibe.setup.auth import (
|
|||
HttpBrowserSignInGateway,
|
||||
)
|
||||
|
||||
AUTH_ORIGIN = "https://api.mistral.ai"
|
||||
AUTH_ORIGIN = "https://console.mistral.ai"
|
||||
AUTH_BROWSER_BASE_URL = "https://console.mistral.ai"
|
||||
AUTH_API_BASE_URL = AUTH_ORIGIN
|
||||
AUTH_API_BASE_URL = "https://console.mistral.ai/api"
|
||||
TEST_PROCESS_ID = "process-1"
|
||||
TEST_COMPLETE_TOKEN = "complete-token-1"
|
||||
TEST_STATE = "state-1"
|
||||
|
|
@ -46,7 +46,7 @@ def build_sign_in_url(
|
|||
def build_poll_url(
|
||||
*, poll_token: str = TEST_POLL_TOKEN, api_base_url: str = AUTH_API_BASE_URL
|
||||
) -> str:
|
||||
return f"{api_base_url}/api/vibe/sign-in/poll/{poll_token}"
|
||||
return f"{api_base_url}/vibe/sign-in/poll/{poll_token}"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
|
@ -72,7 +72,7 @@ async def test_http_api_creates_process_with_pkce_payload() -> None:
|
|||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
nonlocal captured_body
|
||||
assert request.url.path == "/vibe/sign-in"
|
||||
assert request.url.path == "/api/vibe/sign-in"
|
||||
captured_body = request.content.decode("utf-8")
|
||||
return httpx.Response(
|
||||
200,
|
||||
|
|
@ -129,7 +129,7 @@ async def test_http_api_exchanges_token_for_api_key() -> None:
|
|||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
nonlocal captured_body
|
||||
assert request.url.path == "/vibe/sign-in/process-1/exchange"
|
||||
assert request.url.path == "/api/vibe/sign-in/process-1/exchange"
|
||||
captured_body = request.content.decode("utf-8")
|
||||
return httpx.Response(200, json={"api_key": "sk-browser-key"})
|
||||
|
||||
|
|
@ -142,6 +142,28 @@ async def test_http_api_exchanges_token_for_api_key() -> None:
|
|||
assert '"code_verifier":"verifier-1"' in captured_body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_logs_exchange_failure_status_and_detail_without_secrets(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
exchange_token = "exchange-token-secret"
|
||||
code_verifier = "verifier-secret"
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.url.path == "/api/vibe/sign-in/process-1/exchange"
|
||||
return httpx.Response(400, json={"detail": "Invalid exchange token."})
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
with caplog.at_level(logging.WARNING, logger="vibe"):
|
||||
with pytest.raises(BrowserSignInError, match="exchange browser sign-in"):
|
||||
await gateway.exchange("process-1", exchange_token, code_verifier)
|
||||
|
||||
assert "status_code=400" in caplog.text
|
||||
assert "response_detail=Invalid exchange token." in caplog.text
|
||||
assert exchange_token not in caplog.text
|
||||
assert code_verifier not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_translates_transport_errors() -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
|
|
@ -232,31 +254,35 @@ async def test_http_api_accepts_poll_url_under_configured_api_base_url() -> None
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_accepts_poll_url_under_configured_api_base_path() -> None:
|
||||
async def test_http_api_accepts_backend_poll_url_under_custom_configured_api_base_path() -> (
|
||||
None
|
||||
):
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
browser_base_url = "https://browser-auth.example/sign-in"
|
||||
api_base_url = "https://browser-auth.example/custom/api"
|
||||
poll_url = f"{api_base_url}/backend-owned-poll/{TEST_POLL_TOKEN}"
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.url.path == "/v1/vibe/sign-in"
|
||||
assert request.url.path == "/custom/api/vibe/sign-in"
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(),
|
||||
"poll_url": build_poll_url(
|
||||
poll_token=TEST_POLL_TOKEN, api_base_url="https://api.mistral.ai/v1"
|
||||
),
|
||||
"sign_in_url": build_sign_in_url(base_url=browser_base_url),
|
||||
"poll_url": poll_url,
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
origin="https://browser-auth.example",
|
||||
browser_base_url=browser_base_url,
|
||||
api_base_url=api_base_url,
|
||||
) as gateway:
|
||||
process = await gateway.create_process("challenge-123")
|
||||
|
||||
assert process.poll_url == build_poll_url(api_base_url="https://api.mistral.ai/v1")
|
||||
assert process.poll_url == poll_url
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -273,7 +299,9 @@ async def test_http_api_accepts_same_origin_urls_with_explicit_default_https_por
|
|||
"sign_in_url": build_sign_in_url(
|
||||
base_url="https://console.mistral.ai:443"
|
||||
),
|
||||
"poll_url": build_poll_url(api_base_url="https://api.mistral.ai:443"),
|
||||
"poll_url": build_poll_url(
|
||||
api_base_url="https://console.mistral.ai:443/api"
|
||||
),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
|
@ -284,7 +312,9 @@ async def test_http_api_accepts_same_origin_urls_with_explicit_default_https_por
|
|||
assert process.sign_in_url == build_sign_in_url(
|
||||
base_url="https://console.mistral.ai:443"
|
||||
)
|
||||
assert process.poll_url == build_poll_url(api_base_url="https://api.mistral.ai:443")
|
||||
assert process.poll_url == build_poll_url(
|
||||
api_base_url="https://console.mistral.ai:443/api"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -299,8 +329,8 @@ async def test_http_api_accepts_poll_url_without_explicit_default_https_port_whe
|
|||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
origin="https://api.mistral.ai:443",
|
||||
api_base_url="https://api.mistral.ai:443",
|
||||
origin="https://console.mistral.ai:443",
|
||||
api_base_url="https://console.mistral.ai:443/api",
|
||||
) as gateway:
|
||||
result = await gateway.poll(build_poll_url())
|
||||
|
||||
|
|
@ -365,15 +395,13 @@ async def test_http_api_rejects_sign_in_url_outside_browser_base_path_after_norm
|
|||
"sign_in_url": build_sign_in_url(
|
||||
base_url="https://console.mistral.ai/v1/.."
|
||||
),
|
||||
"poll_url": build_poll_url(api_base_url="https://api.mistral.ai/v1"),
|
||||
"poll_url": build_poll_url(),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
browser_base_url="https://console.mistral.ai/v1",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
handler, browser_base_url="https://console.mistral.ai/v1"
|
||||
) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
|
@ -395,15 +423,13 @@ async def test_http_api_rejects_sign_in_url_with_encoded_dot_segments_outside_br
|
|||
"sign_in_url": build_sign_in_url(
|
||||
base_url="https://console.mistral.ai/v1/%2e%2e"
|
||||
),
|
||||
"poll_url": build_poll_url(api_base_url="https://api.mistral.ai/v1"),
|
||||
"poll_url": build_poll_url(),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
browser_base_url="https://console.mistral.ai/v1",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
handler, browser_base_url="https://console.mistral.ai/v1"
|
||||
) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
|
@ -449,7 +475,7 @@ async def test_http_api_rejects_returned_poll_url_outside_api_base_path_after_no
|
|||
),
|
||||
"poll_url": build_poll_url(
|
||||
poll_token=TEST_POLL_TOKEN,
|
||||
api_base_url="https://api.mistral.ai/v1/..",
|
||||
api_base_url="https://console.mistral.ai/api/..",
|
||||
),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
|
|
@ -457,8 +483,7 @@ async def test_http_api_rejects_returned_poll_url_outside_api_base_path_after_no
|
|||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
origin="https://console.mistral.ai",
|
||||
browser_base_url="https://console.mistral.ai/v1",
|
||||
) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
|
|
@ -483,7 +508,7 @@ async def test_http_api_rejects_returned_poll_url_with_encoded_dot_segments_outs
|
|||
),
|
||||
"poll_url": build_poll_url(
|
||||
poll_token=TEST_POLL_TOKEN,
|
||||
api_base_url="https://api.mistral.ai/v1/%2e%2e",
|
||||
api_base_url="https://console.mistral.ai/api/%2e%2e",
|
||||
),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
|
|
@ -491,8 +516,7 @@ async def test_http_api_rejects_returned_poll_url_with_encoded_dot_segments_outs
|
|||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
origin="https://console.mistral.ai",
|
||||
browser_base_url="https://console.mistral.ai/v1",
|
||||
) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
|
|
@ -504,15 +528,14 @@ async def test_http_api_rejects_returned_poll_url_with_encoded_dot_segments_outs
|
|||
@pytest.mark.asyncio
|
||||
async def test_http_api_rejects_poll_url_outside_api_base_path() -> None:
|
||||
async with build_gateway(
|
||||
lambda _: httpx.Response(200),
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
lambda _: httpx.Response(200, json={"status": "completed"}),
|
||||
origin="https://console.mistral.ai",
|
||||
) as gateway:
|
||||
with pytest.raises(
|
||||
BrowserSignInError, match="status could not be retrieved"
|
||||
) as err:
|
||||
await gateway.poll(
|
||||
"https://api.mistral.ai/v1evil/api/vibe/sign-in/poll/poll-token-1"
|
||||
"https://console.mistral.ai/apievil/vibe/sign-in/poll/poll-token-1"
|
||||
)
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.POLL_FAILED
|
||||
|
|
@ -523,15 +546,14 @@ async def test_http_api_rejects_poll_url_outside_api_base_path_after_normalizati
|
|||
None
|
||||
):
|
||||
async with build_gateway(
|
||||
lambda _: httpx.Response(200),
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
lambda _: httpx.Response(200, json={"status": "completed"}),
|
||||
origin="https://console.mistral.ai",
|
||||
) as gateway:
|
||||
with pytest.raises(
|
||||
BrowserSignInError, match="status could not be retrieved"
|
||||
) as err:
|
||||
await gateway.poll(
|
||||
"https://api.mistral.ai/v1/../api/vibe/sign-in/poll/poll-token-1"
|
||||
"https://console.mistral.ai/api/../evil/sign-in/poll/poll-token-1"
|
||||
)
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.POLL_FAILED
|
||||
|
|
@ -547,7 +569,7 @@ async def test_http_api_translates_invalid_returned_poll_url_port() -> None:
|
|||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(),
|
||||
"poll_url": "https://api.mistral.ai:99999/api/vibe/sign-in/poll/poll-token-1",
|
||||
"poll_url": "https://console.mistral.ai:99999/api/vibe/sign-in/poll/poll-token-1",
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
|
@ -566,7 +588,7 @@ async def test_http_api_assigns_poll_failed_code_on_invalid_poll_url_port() -> N
|
|||
BrowserSignInError, match="status could not be retrieved"
|
||||
) as err:
|
||||
await gateway.poll(
|
||||
"https://api.mistral.ai:99999/api/vibe/sign-in/poll/poll-token-1"
|
||||
"https://console.mistral.ai:99999/api/vibe/sign-in/poll/poll-token-1"
|
||||
)
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.POLL_FAILED
|
||||
|
|
@ -639,16 +661,15 @@ async def test_http_api_does_not_log_poll_secret_on_poll_url_validation_failure(
|
|||
poll_token = "poll-token-secret"
|
||||
|
||||
async with build_gateway(
|
||||
lambda _: httpx.Response(200),
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
lambda _: httpx.Response(200, json={"status": "completed"}),
|
||||
origin="https://console.mistral.ai",
|
||||
) as gateway:
|
||||
with caplog.at_level(logging.WARNING, logger="vibe"):
|
||||
with pytest.raises(
|
||||
BrowserSignInError, match="status could not be retrieved"
|
||||
):
|
||||
await gateway.poll(
|
||||
f"https://api.mistral.ai/v1evil/api/vibe/sign-in/poll/{poll_token}"
|
||||
f"https://console.mistral.ai/apievil/sign-in/poll/{poll_token}"
|
||||
)
|
||||
|
||||
assert poll_token not in caplog.text
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue