mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
fix: preserve Horizon command contracts
This commit is contained in:
parent
8b930d207e
commit
e532e740be
5 changed files with 40 additions and 6 deletions
|
|
@ -94,9 +94,11 @@ fastmcp whoami
|
||||||
fastmcp logout
|
fastmcp logout
|
||||||
```
|
```
|
||||||
|
|
||||||
`fastmcp login` always shows a verification URL and code.
|
`fastmcp login` first uses `HORIZON_API_KEY` or a valid stored key when one is available.
|
||||||
|
When login needs a new key, it shows a verification URL and code.
|
||||||
It opens a browser when the terminal supports it.
|
It opens a browser when the terminal supports it.
|
||||||
If the browser does not open, use the shown URL and code on another device.
|
If the browser does not open, use the shown URL and code on another device.
|
||||||
|
To switch accounts, run `fastmcp logout` before you run `fastmcp login` again.
|
||||||
|
|
||||||
Use `--host` to connect to another Horizon environment.
|
Use `--host` to connect to another Horizon environment.
|
||||||
The CLI saves the host and clears the stored key before a host change.
|
The CLI saves the host and clears the stored key before a host change.
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ Sign in to Horizon from the FastMCP CLI with the device authorization flow.
|
||||||
fastmcp login
|
fastmcp login
|
||||||
```
|
```
|
||||||
|
|
||||||
The command shows a verification URL and code before it opens the browser.
|
The command uses an environment key or a valid stored key when one is available.
|
||||||
|
When login needs a new key, it shows a verification URL and code before it opens the browser.
|
||||||
If the browser cannot open, visit the shown URL and enter the code.
|
If the browser cannot open, visit the shown URL and enter the code.
|
||||||
|
To switch accounts, run `fastmcp logout` before you run `fastmcp login` again.
|
||||||
New users can register and create their first Horizon organization in the browser.
|
New users can register and create their first Horizon organization in the browser.
|
||||||
Use `fastmcp login --host <url>` to save a different Horizon host.
|
Use `fastmcp login --host <url>` to save a different Horizon host.
|
||||||
A host change clears the stored credential before login.
|
A host change clears the stored credential before login.
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,7 @@ async def login(
|
||||||
credential = await resolve_credential(
|
credential = await resolve_credential(
|
||||||
credentials,
|
credentials,
|
||||||
authorize=device_authorization,
|
authorize=device_authorization,
|
||||||
|
expected_api_origin=configuration.api_origin,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -234,6 +235,7 @@ async def login(
|
||||||
credential = await resolve_credential(
|
credential = await resolve_credential(
|
||||||
credentials,
|
credentials,
|
||||||
authorize=device_authorization,
|
authorize=device_authorization,
|
||||||
|
expected_api_origin=configuration.api_origin,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
user = await _get_user(
|
user = await _get_user(
|
||||||
|
|
@ -277,7 +279,14 @@ async def whoami(
|
||||||
)
|
)
|
||||||
except HorizonUnauthorizedError as error:
|
except HorizonUnauthorizedError as error:
|
||||||
if credential is not None and credential.source == "stored":
|
if credential is not None and credential.source == "stored":
|
||||||
credentials.clear()
|
try:
|
||||||
|
credentials.clear()
|
||||||
|
except StateFileError as cleanup_error:
|
||||||
|
_fail_for_expected_error(
|
||||||
|
"whoami",
|
||||||
|
cleanup_error,
|
||||||
|
json_output=json_output,
|
||||||
|
)
|
||||||
_fail_for_expected_error("whoami", error, json_output=json_output)
|
_fail_for_expected_error("whoami", error, json_output=json_output)
|
||||||
except (
|
except (
|
||||||
AuthenticationRequiredError,
|
AuthenticationRequiredError,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import fastmcp.cli.deploy.command as command_module
|
||||||
from fastmcp.cli.deploy.command import login, logout, whoami
|
from fastmcp.cli.deploy.command import login, logout, whoami
|
||||||
from fastmcp.cli.deploy.credentials import CredentialStore
|
from fastmcp.cli.deploy.credentials import CredentialStore
|
||||||
from fastmcp.cli.deploy.horizon_client import HorizonClient
|
from fastmcp.cli.deploy.horizon_client import HorizonClient
|
||||||
|
from fastmcp.cli.deploy.state import StateFileError
|
||||||
|
|
||||||
|
|
||||||
class HorizonAuthAPI:
|
class HorizonAuthAPI:
|
||||||
|
|
@ -259,6 +260,26 @@ async def test_login_never_persists_an_environment_key(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_json_whoami_reports_a_failed_rejected_key_cleanup(
|
||||||
|
use_horizon_api: Callable[[HorizonAuthAPI], None],
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
use_horizon_api(HorizonAuthAPI(invalid_api_key="fmcp_stale_key"))
|
||||||
|
CredentialStore().save("fmcp_stale_key")
|
||||||
|
|
||||||
|
def fail_clear(store: CredentialStore) -> None:
|
||||||
|
raise StateFileError("cleanup failed")
|
||||||
|
|
||||||
|
monkeypatch.setattr(CredentialStore, "clear", fail_clear)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match="1"):
|
||||||
|
await whoami(json_output=True)
|
||||||
|
|
||||||
|
result = json.loads(capsys.readouterr().out)
|
||||||
|
assert result["error"]["category"] == "state_error"
|
||||||
|
|
||||||
|
|
||||||
async def test_json_whoami_does_not_start_device_authorization(
|
async def test_json_whoami_does_not_start_device_authorization(
|
||||||
capsys: pytest.CaptureFixture[str],
|
capsys: pytest.CaptureFixture[str],
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ def test_tty_device_challenge_uses_the_sign_in_layout(
|
||||||
emit_device_challenge(authorization(), json_output=False)
|
emit_device_challenge(authorization(), json_output=False)
|
||||||
|
|
||||||
output = capsys.readouterr().out
|
output = capsys.readouterr().out
|
||||||
assert "╭" in output
|
assert "│" in output
|
||||||
assert "Deploy FastMCP on Horizon" in output
|
assert "Deploy FastMCP on Horizon" in output
|
||||||
assert "✓ Device authorization started" in output
|
assert "✓ Device authorization started" in output
|
||||||
assert "https://horizon.prefect.io/oauth/device?user_code=ABCD-EFGH" in output
|
assert "https://horizon.prefect.io/oauth/device?user_code=ABCD-EFGH" in output
|
||||||
|
|
@ -82,7 +82,7 @@ def test_tty_identity_uses_an_account_panel(
|
||||||
emit_identity("whoami", user(), json_output=False)
|
emit_identity("whoami", user(), json_output=False)
|
||||||
|
|
||||||
output = capsys.readouterr().out
|
output = capsys.readouterr().out
|
||||||
assert "╭" in output
|
assert "│" in output
|
||||||
assert "Horizon Account" in output
|
assert "Horizon Account" in output
|
||||||
assert "Ada" in output
|
assert "Ada" in output
|
||||||
assert "ada@example.com" in output
|
assert "ada@example.com" in output
|
||||||
|
|
@ -124,7 +124,7 @@ def test_tty_logout_uses_the_horizon_header(
|
||||||
|
|
||||||
output = capsys.readouterr().out
|
output = capsys.readouterr().out
|
||||||
assert "Logged out of Horizon" in output
|
assert "Logged out of Horizon" in output
|
||||||
assert "╭" in output
|
assert "│" in output
|
||||||
|
|
||||||
|
|
||||||
def test_json_logout_has_stable_fields(
|
def test_json_logout_has_stable_fields(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue