feat(host-agent): wire MCP server into create_application
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ from host_agent.identity import HostIdentityStore
|
|||||||
from host_agent.instance_lock import InstanceLock
|
from host_agent.instance_lock import InstanceLock
|
||||||
from host_agent.lease import ActiveAssignmentRunner
|
from host_agent.lease import ActiveAssignmentRunner
|
||||||
from host_agent.local_account import LocalAccountStore
|
from host_agent.local_account import LocalAccountStore
|
||||||
|
from host_agent.mcp_lock import McpBusyTracker
|
||||||
|
from host_agent.mcp_token import McpTokenStore
|
||||||
from host_agent.policy_cache import HostPolicyCacheStore
|
from host_agent.policy_cache import HostPolicyCacheStore
|
||||||
from host_agent.processor import AssignmentProcessingResult, AssignmentProcessor
|
from host_agent.processor import AssignmentProcessingResult, AssignmentProcessor
|
||||||
from host_agent.retention import prune_task_history
|
from host_agent.retention import prune_task_history
|
||||||
@@ -28,6 +31,7 @@ from host_agent.skill_sync import HostAgentSkillSync
|
|||||||
from host_agent.status import AgentStatusTracker
|
from host_agent.status import AgentStatusTracker
|
||||||
from host_agent.web.app import create_console_app
|
from host_agent.web.app import create_console_app
|
||||||
from host_agent.web.auth import SessionManager
|
from host_agent.web.auth import SessionManager
|
||||||
|
from host_agent.web.mcp import build_mcp_server
|
||||||
from storage.artifact_store import ArtifactStore
|
from storage.artifact_store import ArtifactStore
|
||||||
from storage.device_config import DeviceConfigStore
|
from storage.device_config import DeviceConfigStore
|
||||||
from storage.task_metadata import TaskMetadataStore
|
from storage.task_metadata import TaskMetadataStore
|
||||||
@@ -201,13 +205,28 @@ def create_application(
|
|||||||
db_path=resolved_config.task_progress_db_path
|
db_path=resolved_config.task_progress_db_path
|
||||||
)
|
)
|
||||||
timeline = Timeline(ArtifactStore(root=resolved_config.task_artifact_dir))
|
timeline = Timeline(ArtifactStore(root=resolved_config.task_artifact_dir))
|
||||||
|
mcp_token_path = resolved_config.identity_path.parent / "host_mcp_token.json"
|
||||||
|
mcp_token_existed = mcp_token_path.exists()
|
||||||
|
mcp_token_store = McpTokenStore(mcp_token_path)
|
||||||
|
mcp_token_store.load_or_create()
|
||||||
|
if not mcp_token_existed:
|
||||||
|
logging.getLogger(__name__).info(
|
||||||
|
"MCP token generated at %s", mcp_token_path
|
||||||
|
)
|
||||||
|
mcp_busy_tracker = McpBusyTracker(ttl_seconds=60.0)
|
||||||
executor = AssignmentExecutor(
|
executor = AssignmentExecutor(
|
||||||
create_execution_factories(
|
create_execution_factories(
|
||||||
resolved_manager,
|
resolved_manager,
|
||||||
metadata_store=metadata_store,
|
metadata_store=metadata_store,
|
||||||
timeline=timeline,
|
timeline=timeline,
|
||||||
host_agent_config=resolved_config,
|
host_agent_config=resolved_config,
|
||||||
)
|
),
|
||||||
|
mcp_busy_tracker=mcp_busy_tracker,
|
||||||
|
)
|
||||||
|
mcp_server = build_mcp_server(
|
||||||
|
manager=resolved_manager,
|
||||||
|
mcp_busy_tracker=mcp_busy_tracker,
|
||||||
|
status_tracker=status_tracker,
|
||||||
)
|
)
|
||||||
console_app = create_console_app(
|
console_app = create_console_app(
|
||||||
config=resolved_config,
|
config=resolved_config,
|
||||||
@@ -225,6 +244,9 @@ def create_application(
|
|||||||
metadata_store=metadata_store,
|
metadata_store=metadata_store,
|
||||||
timeline=timeline,
|
timeline=timeline,
|
||||||
executor=executor,
|
executor=executor,
|
||||||
|
mcp_server=mcp_server,
|
||||||
|
mcp_token_store=mcp_token_store,
|
||||||
|
mcp_busy_tracker=mcp_busy_tracker,
|
||||||
)
|
)
|
||||||
console_server = _EmbeddedConsoleServer(
|
console_server = _EmbeddedConsoleServer(
|
||||||
uvicorn.Config(
|
uvicorn.Config(
|
||||||
@@ -240,6 +262,7 @@ def create_application(
|
|||||||
client,
|
client,
|
||||||
resolved_config,
|
resolved_config,
|
||||||
status_tracker=status_tracker,
|
status_tracker=status_tracker,
|
||||||
|
mcp_busy_tracker=mcp_busy_tracker,
|
||||||
on_sync=lambda device_count: history_store.record_heartbeat(
|
on_sync=lambda device_count: history_store.record_heartbeat(
|
||||||
device_count=device_count
|
device_count=device_count
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from datetime import UTC, datetime, timedelta
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from cloud.internal_api.models import (
|
from cloud.internal_api.models import (
|
||||||
AssignmentModel,
|
AssignmentModel,
|
||||||
@@ -15,10 +16,23 @@ from cloud.internal_api.models import (
|
|||||||
)
|
)
|
||||||
from device.manager import DeviceManager
|
from device.manager import DeviceManager
|
||||||
from host_agent.app import HostAgentApplication, create_application
|
from host_agent.app import HostAgentApplication, create_application
|
||||||
|
from host_agent.assignment import AssignmentExecutor
|
||||||
from host_agent.config import HostAgentConfig
|
from host_agent.config import HostAgentConfig
|
||||||
|
from host_agent.execution import create_execution_factories
|
||||||
|
from host_agent.history import ConsoleHistoryStore
|
||||||
from host_agent.identity import HostIdentityStore
|
from host_agent.identity import HostIdentityStore
|
||||||
from host_agent.instance_lock import InstanceAlreadyRunningError
|
from host_agent.instance_lock import InstanceAlreadyRunningError
|
||||||
|
from host_agent.local_account import LocalAccountStore
|
||||||
|
from host_agent.mcp_lock import McpBusyTracker
|
||||||
|
from host_agent.mcp_token import McpTokenStore
|
||||||
|
from host_agent.status import AgentStatusTracker
|
||||||
|
from host_agent.web.app import create_console_app
|
||||||
|
from host_agent.web.auth import SessionManager
|
||||||
|
from host_agent.web.mcp import build_mcp_server
|
||||||
|
from storage.artifact_store import ArtifactStore
|
||||||
from storage.device_config import DeviceConfigStore
|
from storage.device_config import DeviceConfigStore
|
||||||
|
from storage.task_metadata import TaskMetadataStore
|
||||||
|
from storage.timeline import Timeline
|
||||||
|
|
||||||
|
|
||||||
def _free_loopback_port() -> int:
|
def _free_loopback_port() -> int:
|
||||||
@@ -653,6 +667,79 @@ def test_create_application_with_independent_identity_paths_coexist(
|
|||||||
asyncio.run(app_a.client.aclose())
|
asyncio.run(app_a.client.aclose())
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_application_wires_mcp_components(tmp_path, monkeypatch) -> None:
|
||||||
|
"""create_application produces a console app with /mcp mounted (auth-protected)
|
||||||
|
and persists the host_mcp_token.json file alongside the identity."""
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
config = _config()
|
||||||
|
config_store = DeviceConfigStore(tmp_path / "devices.sqlite3")
|
||||||
|
identity_store = HostIdentityStore(config.identity_path)
|
||||||
|
history_store = ConsoleHistoryStore(
|
||||||
|
tmp_path / "host_console_history.sqlite3",
|
||||||
|
limit=config.console_history_limit,
|
||||||
|
)
|
||||||
|
metadata_store = TaskMetadataStore(db_path=config.task_progress_db_path)
|
||||||
|
timeline = Timeline(ArtifactStore(root=config.task_artifact_dir))
|
||||||
|
status_tracker = AgentStatusTracker()
|
||||||
|
|
||||||
|
application = create_application(
|
||||||
|
config=config,
|
||||||
|
device_config_store=config_store,
|
||||||
|
identity_store=identity_store,
|
||||||
|
manager=DeviceManager(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Token file must exist after create_application.
|
||||||
|
assert (config.identity_path.parent / "host_mcp_token.json").exists()
|
||||||
|
|
||||||
|
# Heartbeat must hold the in-process McpBusyTracker.
|
||||||
|
assert application.heartbeat.mcp_busy_tracker is not None
|
||||||
|
|
||||||
|
# Build the same console app the production path builds and verify /mcp
|
||||||
|
# is mounted (responds 401, not 404) without a bearer token.
|
||||||
|
mcp_token_store = McpTokenStore(
|
||||||
|
config.identity_path.parent / "host_mcp_token.json"
|
||||||
|
)
|
||||||
|
mcp_token_store.load_or_create()
|
||||||
|
mcp_busy_tracker = McpBusyTracker(ttl_seconds=60.0)
|
||||||
|
mcp_server = build_mcp_server(
|
||||||
|
manager=application.heartbeat.manager,
|
||||||
|
mcp_busy_tracker=mcp_busy_tracker,
|
||||||
|
status_tracker=status_tracker,
|
||||||
|
)
|
||||||
|
console_app = create_console_app(
|
||||||
|
config=config,
|
||||||
|
manager=application.heartbeat.manager,
|
||||||
|
config_store=config_store,
|
||||||
|
local_account_store=LocalAccountStore(config.local_account_path),
|
||||||
|
identity_store=identity_store,
|
||||||
|
history_store=history_store,
|
||||||
|
status_tracker=status_tracker,
|
||||||
|
session_manager=SessionManager(ttl_seconds=config.console_session_ttl_seconds),
|
||||||
|
enrollment_client=None,
|
||||||
|
host_client=application.client,
|
||||||
|
metadata_store=metadata_store,
|
||||||
|
timeline=timeline,
|
||||||
|
executor=AssignmentExecutor(
|
||||||
|
create_execution_factories(
|
||||||
|
application.heartbeat.manager,
|
||||||
|
metadata_store=metadata_store,
|
||||||
|
timeline=timeline,
|
||||||
|
host_agent_config=config,
|
||||||
|
),
|
||||||
|
mcp_busy_tracker=mcp_busy_tracker,
|
||||||
|
),
|
||||||
|
mcp_server=mcp_server,
|
||||||
|
mcp_token_store=mcp_token_store,
|
||||||
|
mcp_busy_tracker=mcp_busy_tracker,
|
||||||
|
)
|
||||||
|
with TestClient(console_app) as client:
|
||||||
|
resp = client.post("/mcp/")
|
||||||
|
assert resp.status_code == 401 # auth required, not 404
|
||||||
|
|
||||||
|
asyncio.run(application.client.aclose())
|
||||||
|
|
||||||
|
|
||||||
def test_lock_released_after_run_async_allows_restart(tmp_path, monkeypatch) -> None:
|
def test_lock_released_after_run_async_allows_restart(tmp_path, monkeypatch) -> None:
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
identity_path = tmp_path / "host_identity.json"
|
identity_path = tmp_path / "host_identity.json"
|
||||||
|
|||||||
Reference in New Issue
Block a user