Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions monitor/runtime_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import annotations

import os


def resolve_server_port(configured_port: int) -> int:
"""Return the candidate-isolated monitor port when the runtime provides one."""

raw_port = os.environ.get("FITBIT_MONITOR_PORT")
if raw_port is None:
return configured_port
port = int(raw_port)
if not 1 <= port <= 65535:
raise ValueError("FITBIT_MONITOR_PORT 必须在 1..65535")
return port
9 changes: 7 additions & 2 deletions monitor/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import uvicorn

from runtime_env import resolve_server_port
import sleep_model
import retrain_guard
import build_sleep_diff_report
Expand Down Expand Up @@ -465,8 +467,11 @@ def _load_runtime_config() -> dict:
SERVER_HOST = str(
_get_cfg(CONFIG, ("server", "host"), DEFAULT_CONFIG["server"]["host"])
)
SERVER_PORT = _as_int(
_get_cfg(CONFIG, ("server", "port"), DEFAULT_CONFIG["server"]["port"]), 18765
SERVER_PORT = resolve_server_port(
_as_int(
_get_cfg(CONFIG, ("server", "port"), DEFAULT_CONFIG["server"]["port"]),
18765,
)
)
SERVER_LOG_LEVEL = str(
_get_cfg(CONFIG, ("server", "log_level"), DEFAULT_CONFIG["server"]["log_level"])
Expand Down
16 changes: 14 additions & 2 deletions plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ class FitbitConfig(BaseModel):
class FitbitPlugin(Plugin):
api_version = 2
name = "fitbit"
version = "1.4.1"
version = "1.4.2"
desc = "Fitbit health monitor and sleep model"
ConfigModel = FitbitConfig

Expand All @@ -220,7 +220,18 @@ def dashboard_module(cls) -> str:

@classmethod
def mcp_servers(cls) -> list[McpServerSpec]:
return [McpServerSpec(name="fitbit", command=("python", "run_mcp.py"))]
return [
McpServerSpec(
name="fitbit",
command=("python", "run_mcp.py"),
candidate_read_only_tools=(
"get_proactive_events",
"get_sleep_context",
"fitbit_health_snapshot",
"fitbit_sleep_report",
),
)
]

@classmethod
def managed_services(cls) -> list[ManagedServiceSpec]:
Expand All @@ -231,6 +242,7 @@ def managed_services(cls) -> list[ManagedServiceSpec]:
cwd="monitor",
readiness_url="http://127.0.0.1:18765/api/data",
startup_timeout_seconds=15,
validation_port_env="FITBIT_MONITOR_PORT",
)
]

Expand Down
12 changes: 10 additions & 2 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ def test_declares_mcp_and_both_proactive_channels() -> None:
plugin = FitbitPlugin()
plugin.context = type("Context", (), {"config": FitbitConfig()})()

assert plugin.version == "1.4.1"
assert [server.name for server in plugin.mcp_servers()] == ["fitbit"]
assert plugin.version == "1.4.2"
servers = plugin.mcp_servers()
assert [server.name for server in servers] == ["fitbit"]
assert servers[0].candidate_read_only_tools == (
"get_proactive_events",
"get_sleep_context",
"fitbit_health_snapshot",
"fitbit_sleep_report",
)
services = plugin.managed_services()
assert [(service.id, service.cwd) for service in services] == [
("monitor", "monitor")
]
assert services[0].validation_port_env == "FITBIT_MONITOR_PORT"
sources = plugin.proactive_sources()
assert [source.id for source in sources] == ["health_alerts", "sleep_context"]
assert [source.channels for source in sources] == [("alert",), ("context",)]
Expand Down
32 changes: 32 additions & 0 deletions tests/test_runtime_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

import pytest

from monitor.runtime_env import resolve_server_port


def test_server_port_uses_config_without_runtime_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("FITBIT_MONITOR_PORT", raising=False)

assert resolve_server_port(18765) == 18765


def test_server_port_uses_candidate_isolation_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("FITBIT_MONITOR_PORT", "28765")

assert resolve_server_port(18765) == 28765


@pytest.mark.parametrize("value", ["0", "65536", "invalid"])
def test_server_port_rejects_invalid_runtime_override(
monkeypatch: pytest.MonkeyPatch,
value: str,
) -> None:
monkeypatch.setenv("FITBIT_MONITOR_PORT", value)

with pytest.raises(ValueError):
resolve_server_port(18765)
Loading