refactor(monitoring): N2 - tach monitoring_tab.py, CanonicalAuditLogger, MonitoringQueryService, go circular import, sandbox matrix

- ui/monitoring_tab.py (1546 dong) tach thanh presentation/monitoring/**
  (container + 7 tab/card + shared helper), ui/monitoring_tab.py con lai
  re-export shim de app.py khong doi.
- infrastructure/telemetry/audit_logger.py: CanonicalAuditLogger, core/audit_log.py
  thanh wrapper mong, tuong thich nguoc 100% voi schema .jsonl cu.
- application/monitoring/monitoring_query_service.py: MonitoringQueryService
  read-only, filter/sort/pagination, khong import PySide6.
- Go circular import model_pricing<->usage_tracker va agent_security<->
  agent_security_alert (core/agent_security_types.py moi).
- infrastructure/sandbox/sandbox_capabilities.py: SandboxCapabilityMatrix
  theo OS (Windows/Linux/macOS), chua dau noi vao core/sandbox_manager.py.
- conftest.py: sua loi checkout khong ten cowork_local khien pytest import
  nham thu muc khac.
- 77 test moi, 167/167 pass. QA da xac nhan UI/business logic khong doi
  (xem evidence/report/unified_report.html).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Hiep Ha Van
2026-08-25 23:52:36 +09:00
co-authored by Claude Sonnet 5
parent 86c27e2e79
commit 40b12ecb15
54 changed files with 3506 additions and 1637 deletions
+1
View File
@@ -0,0 +1 @@
"""Application monitoring package: Monitoring query service for audit and metrics."""
@@ -0,0 +1,47 @@
"""Application-layer view of an audit event — decoupled from the
infrastructure ``CanonicalAuditEvent`` so ``application/`` doesn't need to
share a concrete class with ``infrastructure/`` (only the shape). Field names
match the canonical audit schema (see
``infrastructure/telemetry/audit_logger.py``) 1:1.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict
@dataclass(frozen=True)
class AuditEventDTO:
ts: str
kind: str
name: str
ok: bool
detail: str
agent_role: str = ""
account: str = ""
role: str = ""
machine: str = ""
@classmethod
def from_raw(cls, raw: Dict[str, Any]) -> "AuditEventDTO":
"""Tolerant of missing keys — accepts both a
``CanonicalAuditEvent.to_dict()`` result and any historical raw
``.jsonl`` row."""
return cls(
ts=str(raw.get("ts", "")),
kind=str(raw.get("kind", "")),
name=str(raw.get("name", "")),
ok=bool(raw.get("ok", False)),
detail=str(raw.get("detail", "")),
agent_role=str(raw.get("agent_role", "")),
account=str(raw.get("account", "")),
role=str(raw.get("role", "")),
machine=str(raw.get("machine", "")),
)
def to_dict(self) -> Dict[str, Any]:
return {
"ts": self.ts, "kind": self.kind, "agent_role": self.agent_role,
"name": self.name, "ok": self.ok, "detail": self.detail,
"account": self.account, "role": self.role, "machine": self.machine,
}
@@ -0,0 +1,54 @@
"""Read-only query service over audit events — filter + sort + pagination.
Pure Python: no PySide6 import, no UI code. Depends only on an injected
``AuditEventRepository`` (see ``repository/audit_event_repository.py``), so it
is fully unit-testable with ``InMemoryAuditEventRepository`` and independent
of file I/O or Qt.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional
from .dto.audit_event_dto import AuditEventDTO
from .repository.audit_event_repository import AuditEventRepository
@dataclass(frozen=True)
class Page:
items: List[AuditEventDTO]
total: int
page: int
page_size: int
@property
def has_more(self) -> bool:
return self.page * self.page_size < self.total
class MonitoringQueryService:
"""Read-only. Callers ask for a filtered/sorted/paginated slice of the
audit log; this service never writes anything."""
def __init__(self, repository: AuditEventRepository) -> None:
self._repository = repository
def query(self, kind: Optional[str] = None, ok: Optional[bool] = None,
text: Optional[str] = None, sort_by: str = "ts",
descending: bool = True, page: int = 1, page_size: int = 50) -> Page:
events = self._repository.load(kind=kind)
if ok is not None:
events = [e for e in events if e.ok == ok]
if text:
needle = text.lower()
events = [e for e in events
if needle in e.name.lower() or needle in e.detail.lower()]
events = sorted(events, key=lambda e: getattr(e, sort_by, ""), reverse=descending)
total = len(events)
page = max(1, page)
start = (page - 1) * page_size
items = events[start:start + page_size] if page_size > 0 else events
return Page(items=items, total=total, page=page, page_size=page_size)
@@ -0,0 +1,41 @@
"""Audit-event repository — the boundary between ``MonitoringQueryService``
and where events actually live. ``CanonicalAuditEventRepository`` is the real
adapter (wraps an injected ``CanonicalAuditLogger``); ``InMemoryAuditEventRepository``
is a constructor-injected test double, following this repo's existing
``Fake*``/``Recording*`` convention (see ``tests/routing/*``,
``tests/test_project_context_mcp_template.py``) rather than ``unittest.mock``.
"""
from __future__ import annotations
from typing import List, Optional, Protocol
from ..dto.audit_event_dto import AuditEventDTO
class AuditEventRepository(Protocol):
def load(self, kind: Optional[str] = None) -> List[AuditEventDTO]:
...
class CanonicalAuditEventRepository:
"""Adapter over ``infrastructure.telemetry.audit_logger.CanonicalAuditLogger``
— the only place this application service reaches into infrastructure."""
def __init__(self, audit_logger) -> None:
self._audit_logger = audit_logger
def load(self, kind: Optional[str] = None) -> List[AuditEventDTO]:
events = self._audit_logger.load_events(kind=kind)
return [AuditEventDTO.from_raw(e.to_dict()) for e in events]
class InMemoryAuditEventRepository:
"""Test double — holds a fixed list of events, no file I/O."""
def __init__(self, events: List[AuditEventDTO]) -> None:
self._events = list(events)
def load(self, kind: Optional[str] = None) -> List[AuditEventDTO]:
if kind is None:
return list(self._events)
return [e for e in self._events if e.kind == kind]