This commit is contained in:
@@ -116,6 +116,11 @@ class HostAgentApplication:
|
||||
return await claim
|
||||
|
||||
|
||||
class _EmbeddedConsoleServer(uvicorn.Server):
|
||||
def install_signal_handlers(self) -> None:
|
||||
"""Host Agent owns process-level signal handling."""
|
||||
|
||||
|
||||
def create_application(
|
||||
*,
|
||||
config: HostAgentConfig | None = None,
|
||||
@@ -148,61 +153,49 @@ def create_application(
|
||||
bootstrap_client.close()
|
||||
client = HostAgentClient(resolved_config)
|
||||
|
||||
history_store: ConsoleHistoryStore | None = None
|
||||
status_tracker: AgentStatusTracker | None = None
|
||||
console_server: uvicorn.Server | None = None
|
||||
history_store = ConsoleHistoryStore(
|
||||
resolved_config.identity_path.parent / "host_console_history.sqlite3",
|
||||
limit=resolved_config.console_history_limit,
|
||||
)
|
||||
status_tracker = AgentStatusTracker()
|
||||
console_enrollment_client: HostAgentEnrollmentClient | None = None
|
||||
if resolved_config.console_enabled:
|
||||
history_store = ConsoleHistoryStore(
|
||||
resolved_config.identity_path.parent / "host_console_history.sqlite3",
|
||||
limit=resolved_config.console_history_limit,
|
||||
)
|
||||
status_tracker = AgentStatusTracker()
|
||||
if resolved_config.enrollment_managed:
|
||||
console_enrollment_client = HostAgentEnrollmentClient(resolved_config)
|
||||
console_app = create_console_app(
|
||||
config=resolved_config,
|
||||
manager=resolved_manager,
|
||||
config_store=config_store,
|
||||
local_account_store=LocalAccountStore(resolved_config.local_account_path),
|
||||
identity_store=resolved_identity_store,
|
||||
history_store=history_store,
|
||||
status_tracker=status_tracker,
|
||||
session_manager=SessionManager(
|
||||
ttl_seconds=resolved_config.console_session_ttl_seconds
|
||||
),
|
||||
enrollment_client=console_enrollment_client,
|
||||
)
|
||||
console_server = uvicorn.Server(
|
||||
uvicorn.Config(
|
||||
console_app,
|
||||
host=resolved_config.console_bind_host,
|
||||
port=resolved_config.console_port,
|
||||
log_level="warning",
|
||||
)
|
||||
if resolved_config.enrollment_managed:
|
||||
console_enrollment_client = HostAgentEnrollmentClient(resolved_config)
|
||||
console_app = create_console_app(
|
||||
config=resolved_config,
|
||||
manager=resolved_manager,
|
||||
config_store=config_store,
|
||||
local_account_store=LocalAccountStore(resolved_config.local_account_path),
|
||||
identity_store=resolved_identity_store,
|
||||
history_store=history_store,
|
||||
status_tracker=status_tracker,
|
||||
session_manager=SessionManager(
|
||||
ttl_seconds=resolved_config.console_session_ttl_seconds
|
||||
),
|
||||
enrollment_client=console_enrollment_client,
|
||||
)
|
||||
console_server = _EmbeddedConsoleServer(
|
||||
uvicorn.Config(
|
||||
console_app,
|
||||
host=resolved_config.console_bind_host,
|
||||
port=resolved_config.console_port,
|
||||
log_level="warning",
|
||||
)
|
||||
)
|
||||
|
||||
heartbeat = HeartbeatSynchronizer(
|
||||
resolved_manager,
|
||||
client,
|
||||
resolved_config,
|
||||
status_tracker=status_tracker,
|
||||
on_sync=(
|
||||
(
|
||||
lambda device_count: history_store.record_heartbeat(
|
||||
device_count=device_count
|
||||
)
|
||||
)
|
||||
if history_store is not None
|
||||
else None
|
||||
on_sync=lambda device_count: history_store.record_heartbeat(
|
||||
device_count=device_count
|
||||
),
|
||||
policy_cache=HostPolicyCacheStore(
|
||||
resolved_config.identity_path.parent / "host_governance_policy.json"
|
||||
),
|
||||
on_policy_sync=(
|
||||
(lambda revision: history_store.record_policy_sync(revision=revision))
|
||||
if history_store is not None
|
||||
else None
|
||||
on_policy_sync=lambda revision: history_store.record_policy_sync(
|
||||
revision=revision
|
||||
),
|
||||
)
|
||||
executor = AssignmentExecutor(
|
||||
@@ -216,14 +209,8 @@ def create_application(
|
||||
client,
|
||||
active_runner,
|
||||
status_tracker=status_tracker,
|
||||
on_result=(
|
||||
(
|
||||
lambda assignment, result: _record_assignment_history(
|
||||
history_store, assignment, result
|
||||
)
|
||||
)
|
||||
if history_store is not None
|
||||
else None
|
||||
on_result=lambda assignment, result: _record_assignment_history(
|
||||
history_store, assignment, result
|
||||
),
|
||||
)
|
||||
return HostAgentApplication(
|
||||
|
||||
@@ -29,7 +29,6 @@ class HostAgentConfig:
|
||||
retry_backoff_seconds: float = 1.0
|
||||
max_retry_backoff_seconds: float = 30.0
|
||||
max_retry_attempts: int = 5
|
||||
console_enabled: bool = False
|
||||
console_bind_host: str = "127.0.0.1"
|
||||
console_port: int = 8765
|
||||
console_allow_non_loopback: bool = False
|
||||
@@ -96,7 +95,6 @@ def load_host_agent_config(
|
||||
"HOST_AGENT_MAX_RETRY_ATTEMPTS",
|
||||
5,
|
||||
),
|
||||
console_enabled=_truthy(values, "HOST_AGENT_CONSOLE_ENABLED", False),
|
||||
console_bind_host=values.get(
|
||||
"HOST_AGENT_CONSOLE_BIND_HOST", "127.0.0.1"
|
||||
).strip(),
|
||||
@@ -122,7 +120,7 @@ def load_host_agent_config(
|
||||
raise HostAgentConfigurationError(
|
||||
"maximum retry backoff must not be less than initial backoff"
|
||||
)
|
||||
if config.console_enabled and (
|
||||
if (
|
||||
config.console_bind_host not in _LOOPBACK_BIND_HOSTS
|
||||
and not config.console_allow_non_loopback
|
||||
):
|
||||
|
||||
@@ -362,7 +362,7 @@ def test_main_task_cancellation_waits_for_active_work_shutdown() -> None:
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_console_enabled_serves_http_and_shuts_down_cleanly(tmp_path) -> None:
|
||||
def test_console_serves_http_and_shuts_down_cleanly(tmp_path) -> None:
|
||||
async def scenario() -> None:
|
||||
port = _free_loopback_port()
|
||||
config = HostAgentConfig(
|
||||
@@ -371,7 +371,6 @@ def test_console_enabled_serves_http_and_shuts_down_cleanly(tmp_path) -> None:
|
||||
token="secret",
|
||||
identity_path=tmp_path / "host_identity.json",
|
||||
local_account_path=tmp_path / "host_local_account.json",
|
||||
console_enabled=True,
|
||||
console_bind_host="127.0.0.1",
|
||||
console_port=port,
|
||||
)
|
||||
@@ -449,9 +448,9 @@ def test_console_enabled_serves_http_and_shuts_down_cleanly(tmp_path) -> None:
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_console_disabled_by_default_opens_no_socket(tmp_path, monkeypatch) -> None:
|
||||
def test_console_is_created_by_default(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
application = create_application(config=_config(), manager=DeviceManager())
|
||||
|
||||
assert application.console_server is None
|
||||
assert application.console_server is not None
|
||||
asyncio.run(application.client.aclose())
|
||||
|
||||
@@ -104,10 +104,9 @@ def test_load_host_agent_config_rejects_invalid_values(
|
||||
load_host_agent_config(overrides)
|
||||
|
||||
|
||||
def test_console_defaults_are_disabled_and_do_not_trigger_validation() -> None:
|
||||
def test_console_defaults_are_loopback_bound() -> None:
|
||||
config = load_host_agent_config({})
|
||||
|
||||
assert config.console_enabled is False
|
||||
assert config.console_bind_host == "127.0.0.1"
|
||||
assert config.console_port == 8765
|
||||
assert config.console_allow_non_loopback is False
|
||||
@@ -115,18 +114,10 @@ def test_console_defaults_are_disabled_and_do_not_trigger_validation() -> None:
|
||||
assert config.console_history_limit == 200
|
||||
|
||||
|
||||
def test_console_enabled_with_default_loopback_bind_passes() -> None:
|
||||
config = load_host_agent_config({"HOST_AGENT_CONSOLE_ENABLED": "true"})
|
||||
|
||||
assert config.console_enabled is True
|
||||
assert config.console_bind_host == "127.0.0.1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bind_host", ["127.0.0.1", "localhost", "::1"])
|
||||
def test_console_enabled_with_loopback_bind_host_passes(bind_host: str) -> None:
|
||||
def test_console_loopback_bind_host_passes(bind_host: str) -> None:
|
||||
config = load_host_agent_config(
|
||||
{
|
||||
"HOST_AGENT_CONSOLE_ENABLED": "true",
|
||||
"HOST_AGENT_CONSOLE_BIND_HOST": bind_host,
|
||||
}
|
||||
)
|
||||
@@ -134,20 +125,18 @@ def test_console_enabled_with_loopback_bind_host_passes(bind_host: str) -> None:
|
||||
assert config.console_bind_host == bind_host
|
||||
|
||||
|
||||
def test_console_enabled_with_non_loopback_bind_without_opt_in_raises() -> None:
|
||||
def test_console_non_loopback_bind_without_opt_in_raises() -> None:
|
||||
with pytest.raises(HostAgentConfigurationError):
|
||||
load_host_agent_config(
|
||||
{
|
||||
"HOST_AGENT_CONSOLE_ENABLED": "true",
|
||||
"HOST_AGENT_CONSOLE_BIND_HOST": "0.0.0.0",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_console_enabled_with_non_loopback_bind_with_opt_in_succeeds() -> None:
|
||||
def test_console_non_loopback_bind_with_opt_in_succeeds() -> None:
|
||||
config = load_host_agent_config(
|
||||
{
|
||||
"HOST_AGENT_CONSOLE_ENABLED": "true",
|
||||
"HOST_AGENT_CONSOLE_BIND_HOST": "0.0.0.0",
|
||||
"HOST_AGENT_CONSOLE_ALLOW_NON_LOOPBACK": "true",
|
||||
}
|
||||
@@ -157,24 +146,15 @@ def test_console_enabled_with_non_loopback_bind_with_opt_in_succeeds() -> None:
|
||||
assert config.console_allow_non_loopback is True
|
||||
|
||||
|
||||
def test_console_disabled_with_non_loopback_bind_does_not_raise() -> None:
|
||||
config = load_host_agent_config({"HOST_AGENT_CONSOLE_BIND_HOST": "0.0.0.0"})
|
||||
|
||||
assert config.console_enabled is False
|
||||
assert config.console_bind_host == "0.0.0.0"
|
||||
|
||||
|
||||
def test_console_env_vars_parse_numeric_and_bool_fields() -> None:
|
||||
config = load_host_agent_config(
|
||||
{
|
||||
"HOST_AGENT_CONSOLE_ENABLED": "1",
|
||||
"HOST_AGENT_CONSOLE_PORT": "9001",
|
||||
"HOST_AGENT_CONSOLE_SESSION_TTL_SECONDS": "3600",
|
||||
"HOST_AGENT_CONSOLE_HISTORY_LIMIT": "50",
|
||||
}
|
||||
)
|
||||
|
||||
assert config.console_enabled is True
|
||||
assert config.console_port == 9001
|
||||
assert config.console_session_ttl_seconds == 3600
|
||||
assert config.console_history_limit == 50
|
||||
|
||||
Reference in New Issue
Block a user