Files
minerops/backend/tests/test_agent_commands.py
T
lussierj e898f283ab security: contain physical miner commands in Stage A
Disable physical control through legacy HTTP/M30 producers, typed admission,
queue dispatch and upgraded Windows executors. Restrict diagnostic transports
to exact read-only operations, with ordinal PowerShell validation before sockets.

Retire unsupported pending/claimed work with atomic, idempotent security audits.
Preserve prior delivery evidence and report unknown outcomes after a prior claim,
including explicitly repaired pending rows. Include containment, cancellation,
concurrency and malformed-payload regressions and the old-Agent cutover runbook.

Validation: independent final application-security revalidation PASS. Two unchanged
baseline source-assertion failures are documented as exceptions. Production release
still requires native Windows validation and verified fencing/stop/drain/upgrade;
server cancellation alone cannot revoke a cached command held by an old Agent.
2026-10-15 20:39:45 +00:00

397 lines
16 KiB
Python

"""Focused tests for authenticated Agent queue transport."""
import unittest
from datetime import datetime, timezone
from pathlib import Path
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.agent_auth.service import AgentAuthService
from app.agent_commands.dependencies import (
get_agent_auth_service,
get_agent_command_service,
)
from app.agent_commands.exceptions import (
AgentCommandConflict,
AgentResultValidationError,
)
from app.agent_commands.models import AgentCommandResultRequest, MinerControlResultPayload
from app.agent_commands.repository import CLAIM_COMMANDS_SQL
from app.agent_commands.router import router
from app.agent_commands.service import AgentCommandService
from app.config import settings
NOW = datetime(2026, 8, 24, tzinfo=timezone.utc)
def agent_source_root() -> Path | None:
container_root = Path("/agents/windows")
if container_root.is_dir():
return container_root
repository_root = Path(__file__).parents[2] / "agents" / "windows"
return repository_root if repository_root.is_dir() else None
class FakeRepository:
def __init__(self, row=None):
self.row = row or {
"id": "command-1", "agent_id": "test-agent",
"command_type": "poll_known_miners_now", "command_status": "claimed",
"request_payload": {"action": "suspend"},
}
self.finished = []
def claim_pending(self, agent_id, limit=5):
return [{
"command_id": "command-1", "command_type": "poll_known_miners_now",
"request_payload": {"action": "suspend"}, "created_at": NOW,
}]
def get_command(self, command_id):
return self.row if command_id == self.row.get("id") else None
def finish_command(self, *args):
self.finished.append(args)
if args[2] == "cancelled":
raise AgentCommandConflict(args[4])
def typed_result(**overrides):
result = {
"schema_version": 1,
"command_accepted": True,
"verification_attempted": False,
"verification_status": "unconfirmed",
"observed_service_state": None,
"observed_performance_mode": None,
"failure_code": None,
}
result.update(overrides)
return result
class AgentCommandServiceTests(unittest.TestCase):
def test_claim_query_uses_skip_locked_and_deterministic_order(self):
self.assertIn("FOR UPDATE SKIP LOCKED", CLAIM_COMMANDS_SQL)
self.assertIn("ORDER BY created_at, id", CLAIM_COMMANDS_SQL)
self.assertIn("queue.command_status = 'pending'", CLAIM_COMMANDS_SQL)
def test_typed_result_is_strict_and_sanitized(self):
repository = FakeRepository()
request = AgentCommandResultRequest(
command_id="command-1", status="completed",
result_payload=typed_result(), error_message="raw 192.0.2.10 vendor exception",
)
AgentCommandService(repository).submit_result("test-agent", request)
finished = repository.finished[0]
self.assertIn("[address]", finished[4])
self.assertNotIn("192.0.2.10", str(finished))
def test_wrong_agent_pending_and_terminal_results_are_rejected(self):
request = AgentCommandResultRequest(
command_id="command-1", status="completed", result_payload=typed_result()
)
with self.assertRaises(AgentCommandConflict):
AgentCommandService(FakeRepository()).submit_result("wrong-agent", request)
for queue_status in ("pending", "completed", "failed", "cancelled"):
with self.subTest(status=queue_status), self.assertRaises(AgentCommandConflict):
row = FakeRepository().row.copy()
row["command_status"] = queue_status
AgentCommandService(FakeRepository(row)).submit_result("test-agent", request)
def test_invalid_status_and_secret_fields_are_rejected(self):
with self.assertRaises(Exception):
AgentCommandResultRequest(
command_id="command-1", status="unknown", result_payload={}
)
for forbidden in ("password", "account", "token", "raw_response"):
with self.subTest(field=forbidden):
repository = FakeRepository()
request = AgentCommandResultRequest(
command_id="command-1", status="completed",
result_payload={**typed_result(), forbidden: "sensitive"},
)
with self.assertRaises(ValueError):
MinerControlResultPayload.model_validate(request.result_payload)
def test_suspend_resume_cannot_claim_confirmation(self):
for status in ("confirmed", "verification_failed"):
with self.subTest(status=status):
request = AgentCommandResultRequest(
command_id="command-1", status="completed",
result_payload=typed_result(
verification_attempted=True, verification_status=status
),
)
repository = FakeRepository()
repository.row["command_type"] = "miner_control"
with self.assertRaisesRegex(AgentCommandConflict, "physical_control_disabled_stage_a"):
AgentCommandService(repository).submit_result("test-agent", request)
class FakeAgentAuthRepository:
def get_credential(self, agent_id):
return None
class AgentCommandRouterTests(unittest.TestCase):
def setUp(self):
self.app = FastAPI()
self.app.include_router(router)
self.repository = FakeRepository()
self.app.dependency_overrides[get_agent_command_service] = lambda: AgentCommandService(
self.repository
)
self.app.dependency_overrides[get_agent_auth_service] = lambda: AgentAuthService(
FakeAgentAuthRepository()
)
self.client = TestClient(self.app)
self.old_id = settings.minerops_agent_id
self.old_secret = settings.minerops_agent_shared_secret
settings.minerops_agent_id = "test-agent"
settings.minerops_agent_shared_secret = "test-secret"
def tearDown(self):
settings.minerops_agent_id = self.old_id
settings.minerops_agent_shared_secret = self.old_secret
self.client.close()
def headers(self, agent="test-agent"):
return {
"X-MinerOps-Agent-ID": agent,
"X-MinerOps-Agent-Secret": "test-secret",
}
def test_poll_accepts_secret_only_legacy_agent_and_ignores_query_spoofing(self):
before_claim_calls = []
self.app.state.agent_command_before_claim = lambda: before_claim_calls.append(True)
self.assertEqual(self.client.get("/api/agent/commands/pending").status_code, 401)
self.assertEqual(
self.client.get(
"/api/agent/commands/pending",
headers={"X-MinerOps-Agent-Secret": "test-secret"},
).status_code,
200,
)
self.assertEqual(
self.client.get(
"/api/agent/commands/pending?agent_id=other", headers=self.headers()
).status_code,
200,
)
# Agent polling may claim already queued commands, but must never invoke
# a legacy scheduler/materializer as a side effect.
self.assertEqual(before_claim_calls, [])
self.assertEqual(
self.client.get(
"/api/agent/commands/pending", headers=self.headers("other")
).status_code,
403,
)
def test_result_requires_strict_claimed_contract(self):
response = self.client.post(
"/api/agent/commands/result",
headers=self.headers(),
json={
"command_id": "command-1", "status": "completed",
"result_payload": typed_result(),
},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["status"], "completed")
self.assertEqual(
self.client.post(
"/api/agent/commands/result", headers=self.headers(),
json={"command_id": "command-1", "status": "other",
"result_payload": {}},
).status_code,
422,
)
class AgentSourceContractTests(unittest.TestCase):
def test_agent_typed_path_has_locked_commands_and_no_default_credentials(self):
root = agent_source_root()
if root is None:
self.skipTest("Windows Agent source is not mounted")
service = (root / "MinerOpsAgentService.ps1").read_text()
control = (root / "MinerOpsControl.ps1").read_text()
main = (Path(__file__).parents[1] / "app" / "main.py").read_text()
powershell = "\n".join(path.read_text() for path in root.glob("*.ps1"))
self.assertNotIn('DefaultValue "super"', service)
self.assertNotIn('DefaultValue "super"', powershell)
self.assertIn('"X-MinerOps-Agent-ID"', service)
self.assertIn('.agent_zone_id', service)
self.assertIn('"agent_zone_id"', (root / "config.example.json").read_text())
self.assertNotIn("?agent_id=", service)
self.assertIn("physical_control_disabled_stage_a", control)
self.assertNotIn('"set.miner.', control)
self.assertNotIn("$Payload.performance_mode", control)
self.assertNotIn("api3_account =", control)
self.assertNotIn("api3_password =", control)
for forbidden in ("raw_response", "raw_command", "raw_exception"):
with self.subTest(field=forbidden):
self.assertNotIn(forbidden, control.lower())
self.assertNotIn('"agent_secret": secret', main)
self.assertNotIn('"secret_length":', main)
self.assertIn('"secret_configured": bool(secret)', main)
def test_deployment_preserves_local_state_and_registers_established_tasks(self):
root = agent_source_root()
if root is None:
self.skipTest("Windows Agent source is not mounted")
install = (root / "install.ps1").read_text()
uninstall = (root / "uninstall.ps1").read_text()
for task in (
"MinerOpsAgent", "MinerOpsAgentTray",
"MinerOpsApi3Enrichment", "MinerOpsPoolPoller",
):
self.assertIn(task, install)
self.assertIn(task, uninstall)
for local_path in ("agent-config.json", "agent-state.json", "logs"):
self.assertNotIn(f'"{local_path}"', install)
self.assertNotIn(f'"{local_path}"', uninstall)
self.assertNotIn("agent_secret", install.lower())
self.assertNotIn("api3_password", install.lower())
if __name__ == "__main__":
unittest.main()
class AgentTransportIsolationSourceTests(unittest.TestCase):
def test_known_miners_and_pool_snapshots_are_agent_zone_scoped(self):
main = (Path(__file__).parents[1] / "app" / "main.py").read_text()
identity = (Path(__file__).parents[1] / "app" / "identity.py").read_text()
known_start = main.index('@app.get("/api/agent/known-miners")')
known_end = main.index("# Phase 5J:", known_start)
known = main[known_start:known_end]
pool_start = main.index('@app.post("/api/agent/pool-snapshots")')
pool_end = main.index("\n@app.", pool_start + 1)
pool = main[pool_start:pool_end]
self.assertIn('"/api/agent/known-miners"', identity)
self.assertIn("Depends(require_agent_identity)", known)
self.assertIn("zone_service.routing_context", known)
self.assertIn("miners.agent_zone_id = $1::uuid", known)
self.assertNotIn("minerops_require_agent_secret", known)
self.assertIn("Depends(require_agent_identity)", pool)
self.assertIn("zone_service.routing_context", pool)
self.assertIn("agent_id = authenticated_agent_id", pool)
self.assertIn("agent_zone_id = %s", pool)
self.assertNotIn('body.get("agent_id")', pool)
self.assertNotIn("MINEROPS_AGENT_SHARED_SECRET", pool)
def test_windows_machine_pollers_send_agent_identity(self):
root = agent_source_root()
if root is None:
self.skipTest("Windows Agent source is not mounted")
for filename in (
"MinerOpsPoolPoller.ps1",
"MinerOpsApi3EnrichmentPoller.ps1",
"EnrichOneMiner.ps1",
):
with self.subTest(filename=filename):
source = (root / filename).read_text()
self.assertIn("X-MinerOps-Agent-ID", source)
self.assertIn("X-MinerOps-Agent-Secret", source)
class AgentEnrichmentIsolationSourceTests(unittest.TestCase):
def test_enrichment_routes_require_agent_identity_and_zone_scope(self):
main = (Path(__file__).parents[1] / "app" / "main.py").read_text()
decorators = (
'@app.post("/api/agent/miner-enrichment")',
'@app.post("/api/agent/miner-enrichment-v2")',
'@app.post("/api/agent/miner-enrichment-v3")',
)
for decorator in decorators:
with self.subTest(decorator=decorator):
start = main.index(decorator)
end = main.index("\n@app.", start + 1)
route = main[start:end]
self.assertIn("Depends(require_agent_identity)", route)
self.assertIn("zone_service.routing_context", route)
self.assertIn("agent_zone_id", route)
self.assertNotIn("minerops_require_agent_secret", route)
v2_start = main.index('@app.post("/api/agent/miner-enrichment-v2")')
v2_end = main.index("\n@app.", v2_start + 1)
v2 = main[v2_start:v2_end]
self.assertIn(
"AND agent_zone_id = $2::uuid",
v2,
)
self.assertIn(
"AND agent_zone_id = $3::uuid",
v2,
)
v3_start = main.index('@app.post("/api/agent/miner-enrichment-v3")')
v3_end = main.index("\n@app.", v3_start + 1)
v3 = main[v3_start:v3_end]
self.assertIn(
"AND agent_zone_id = $8::uuid",
v3,
)
self.assertIn(
"AND agent_zone_id = $7::uuid",
v3,
)
def test_api3_uses_authenticated_identity_and_never_global_agent_identity(self):
main = (Path(__file__).parents[1] / "app" / "main.py").read_text()
start = main.index('@app.post("/api/agent/api3-enrichment")')
end = main.index("\n@app.", start + 1)
route = main[start:end]
self.assertIn("Depends(require_agent_identity)", route)
self.assertIn("zone_service.routing_context", route)
self.assertIn("agent_id = authenticated_agent_id", route)
self.assertIn("agent_zone_id = %s", route)
self.assertIn("if miner_id is None:", route)
self.assertIn("continue", route)
self.assertNotIn("MINEROPS_AGENT_SHARED_SECRET", route)
self.assertNotIn('body.get("agent_id")', route)
self.assertNotIn('body.get("site_name")', route)
self.assertNotIn('request.headers.get("x-agent-secret")', route)
def test_legacy_config_remains_explicitly_legacy_only(self):
main = (Path(__file__).parents[1] / "app" / "main.py").read_text()
start = main.index('@app.get("/api/agent/config")')
end = main.index("\n@app.", start + 1)
route = main[start:end]
self.assertIn("minerops_require_agent_secret(request)", route)
self.assertIn('"site_name": "Plexus"', route)
self.assertNotIn("Depends(require_agent_identity)", route)
def test_disabled_old_command_routes_are_fixed_retirement_responses(self):
main = (Path(__file__).parents[1] / "app" / "main.py").read_text()
for decorator in (
'@app.get("/api/agent/commands/pending-disabled-old")',
'@app.post("/api/agent/commands/result-disabled-old")',
):
with self.subTest(decorator=decorator):
start = main.index(decorator)
end = main.index("\n@app.", start + 1)
route = main[start:end]
self.assertIn("status_code=410", route)
self.assertNotIn("minerops_require_agent_secret", route)
self.assertNotIn("request.json", route)
self.assertNotIn("minerops_agent_command_queue", route)