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 @@
"""infrastructure/ — Chạm thế giới thật: file, keyring, HTTP, tiến trình. Cài đặt interface."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure sandbox package: OS-specific sandbox capability adapters."""
@@ -0,0 +1,179 @@
"""Sandbox capability matrix — which isolation backends exist on which OS,
and which one a given risk tier should prefer.
Pure policy/data: no subprocess execution, no PySide6, no dependency on
``core/sandbox_manager.py`` (that module owns the actual execution and isn't
in this task's editable scope — this matrix is a standalone, independently
testable module ready for that module's owner to wire in later).
The Windows entries mirror what ``core/sandbox_manager.py`` +
``core/appcontainer_sandbox.py``/``core/windows_sandbox_vm.py``/
``core/integrity_sandbox.py`` already implement today. Linux/macOS entries
are declared but marked ``implemented=False`` — today those platforms have no
real isolation backend (confirmed: ``core/appcontainer_sandbox.py`` and
``core/windows_sandbox_vm.py`` both hard-return ``False`` off Windows) — so
this matrix reports that honestly instead of pretending capabilities that
don't exist yet. Adding a real Linux/macOS backend later is a 1-line flip of
``implemented`` plus whatever backend module implements it; adding a whole
new OS is a call to :func:`register_profile`, no changes to
:class:`SandboxCapabilityMatrix` itself.
"""
from __future__ import annotations
import sys
from dataclasses import dataclass
from typing import Dict, Optional, Tuple
# Plain string constants (like core/audit_log.py's ``Kind``) rather than an
# Enum, so a brand-new OS can be registered without editing a closed type.
WINDOWS = "windows"
LINUX = "linux"
MACOS = "macos"
UNKNOWN = "unknown"
# Risk tiers — same vocabulary as security/command_risk_classifier.RiskLevel,
# kept as plain strings here so this module has zero dependency on the
# ``security/`` package (out of scope for this task).
SAFE = "SAFE"
MODERATE = "MODERATE"
HIGH = "HIGH"
CRITICAL = "CRITICAL"
BLOCKED = "blocked"
DIRECT = "direct"
def detect_os(platform_name: Optional[str] = None) -> str:
"""``platform_name`` defaults to ``sys.platform`` but can be injected for
testing (e.g. ``detect_os("linux")``, ``detect_os("darwin")``)."""
name = platform_name if platform_name is not None else sys.platform
if name.startswith("win"):
return WINDOWS
if name.startswith("linux"):
return LINUX
if name.startswith("darwin"):
return MACOS
return UNKNOWN
@dataclass(frozen=True)
class SandboxBackend:
name: str
isolation_level: str # "none" | "resource_limits" | "restricted_token" | "namespace" | "seatbelt" | "full_vm"
implemented: bool # whether a real backend exists today, vs. a declared placeholder
@dataclass(frozen=True)
class OsSandboxProfile:
operating_system: str
backends: Tuple[SandboxBackend, ...]
# risk tier -> ordered list of preferred backend names (first available wins)
routing: Dict[str, Tuple[str, ...]]
def _profile(operating_system: str, backends: Tuple[SandboxBackend, ...],
routing: Dict[str, Tuple[str, ...]]) -> OsSandboxProfile:
return OsSandboxProfile(operating_system=operating_system, backends=backends, routing=routing)
_WINDOWS_PROFILE = _profile(
WINDOWS,
backends=(
SandboxBackend(DIRECT, "none", True),
SandboxBackend("integrity_job_wfp", "resource_limits", True),
SandboxBackend("appcontainer", "restricted_token", True),
SandboxBackend("windows_sandbox", "full_vm", True),
),
routing={
SAFE: ("integrity_job_wfp", DIRECT),
MODERATE: ("integrity_job_wfp", DIRECT),
HIGH: ("appcontainer", "integrity_job_wfp"),
CRITICAL: ("windows_sandbox", "appcontainer", BLOCKED),
},
)
_LINUX_PROFILE = _profile(
LINUX,
backends=(
SandboxBackend(DIRECT, "none", True),
SandboxBackend("namespaces_bubblewrap", "namespace", False), # not implemented yet
),
routing={
SAFE: (DIRECT,),
MODERATE: (DIRECT,),
HIGH: ("namespaces_bubblewrap", BLOCKED),
CRITICAL: (BLOCKED,),
},
)
_MACOS_PROFILE = _profile(
MACOS,
backends=(
SandboxBackend(DIRECT, "none", True),
SandboxBackend("sandbox_exec", "seatbelt", False), # not implemented yet
),
routing={
SAFE: (DIRECT,),
MODERATE: (DIRECT,),
HIGH: ("sandbox_exec", BLOCKED),
CRITICAL: (BLOCKED,),
},
)
_UNKNOWN_PROFILE = _profile(
UNKNOWN,
backends=(),
routing={SAFE: (BLOCKED,), MODERATE: (BLOCKED,), HIGH: (BLOCKED,), CRITICAL: (BLOCKED,)},
)
_PROFILES: Dict[str, OsSandboxProfile] = {
WINDOWS: _WINDOWS_PROFILE,
LINUX: _LINUX_PROFILE,
MACOS: _MACOS_PROFILE,
UNKNOWN: _UNKNOWN_PROFILE,
}
def register_profile(profile: OsSandboxProfile) -> None:
"""Extension point for a brand-new OS: build an :class:`OsSandboxProfile`
and register it once — no change to :class:`SandboxCapabilityMatrix`
needed. Overwrites any existing profile for the same
``operating_system`` name (lets a caller override the built-in Windows/
Linux/macOS profiles too, e.g. once a real Linux backend ships)."""
_PROFILES[profile.operating_system] = profile
class SandboxCapabilityMatrix:
"""Answers, for one OS: which backends are actually available today, and
which one a given risk tier should prefer. Read-only policy — does not
execute anything."""
def __init__(self, operating_system: Optional[str] = None,
allow_direct_fallback: bool = True) -> None:
self.operating_system = operating_system if operating_system is not None else detect_os()
self._profile = _PROFILES.get(self.operating_system, _UNKNOWN_PROFILE)
self.allow_direct_fallback = allow_direct_fallback
def all_backends(self) -> Tuple[SandboxBackend, ...]:
"""Every backend declared for this OS, implemented or not."""
return self._profile.backends
def available_backends(self) -> Tuple[SandboxBackend, ...]:
"""Only backends with a real implementation today."""
return tuple(b for b in self._profile.backends if b.implemented)
def select_backend(self, risk_level: str) -> str:
"""The backend name to use for ``risk_level`` on this OS — the first
available (implemented) backend in that tier's preference order, else
``"direct"`` when allowed for a non-CRITICAL tier, else ``"blocked"``."""
available_names = {b.name for b in self.available_backends()}
preferred = self._profile.routing.get(risk_level.upper(), ())
for name in preferred:
if name == BLOCKED:
return BLOCKED
if name in available_names:
return name
if (self.allow_direct_fallback and DIRECT in available_names
and risk_level.upper() != CRITICAL):
return DIRECT
return BLOCKED
+1
View File
@@ -0,0 +1 @@
"""Infrastructure telemetry package: CanonicalAuditLogger and token usage sinks."""
+167
View File
@@ -0,0 +1,167 @@
"""Canonical audit event logging — the infrastructure behind
``core/audit_log.py``'s ``set_identity``/``record``/``load_events`` free
functions (kept as thin wrappers over a module-level singleton for backward
compatibility with every existing call site).
Same on-disk shape as before: one JSON line per event, one file per day
under ``~/.cowork_local/audit/`` (plus a best-effort mirror into a shared
cross-machine folder when an identity's ``shared_dir`` is set). ``record()``
never raises — audit logging must never break a chat turn, a permission
decision, or a tool call.
The event schema is unchanged (same field names, same order) so every
``.jsonl`` file written before this refactor remains fully readable. New
event kinds can be added by defining another ``KIND_*`` constant — nothing
about the schema itself needs to change to support one.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from datetime import date, datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
# Known kinds today. ``kind`` stays a plain str (not an enum) so a caller can
# always pass a new value without editing this module — these constants are
# just the documented, current vocabulary.
KIND_TOOL_CALL = "tool_call"
KIND_PERMISSION = "permission"
KIND_SECURITY_BLOCK = "security_block"
KIND_MCP_CALL = "mcp_call"
@dataclass(frozen=True)
class CanonicalAuditEvent:
"""One audit log entry. Field order matches the pre-refactor
``core/audit_log.py`` schema exactly, for byte-compatible JSON output."""
ts: str
kind: str
agent_role: str
name: str
ok: bool
detail: str
account: str
role: str
machine: str
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,
}
@classmethod
def from_dict(cls, raw: Dict[str, Any]) -> "CanonicalAuditEvent":
"""Tolerant of missing keys, so old/partial rows never fail to load."""
return cls(
ts=str(raw.get("ts", "")),
kind=str(raw.get("kind", "")),
agent_role=str(raw.get("agent_role", "")),
name=str(raw.get("name", "")),
ok=bool(raw.get("ok", False)),
detail=str(raw.get("detail", "")),
account=str(raw.get("account", "")),
role=str(raw.get("role", "")),
machine=str(raw.get("machine", "")),
)
@dataclass
class _Identity:
account: str = ""
role: str = ""
machine: str = ""
shared_dir: str = ""
class CanonicalAuditLogger:
"""Day-sharded JSONL audit writer/reader. Process identity (who's logged
in, this machine's name) is set once via :meth:`set_identity`, mirroring
the pre-refactor module-global pattern but held as instance state so this
class can be constructed/injected instead of relying on globals."""
def __init__(self, audit_dir: Path):
self.audit_dir = Path(audit_dir)
self._identity = _Identity()
def set_identity(self, account: str, machine: str, role: str = "",
shared_dir: str = "") -> None:
"""Called once after login succeeds. ``shared_dir``, when reachable,
makes every subsequent :meth:`record` ALSO best-effort-append to the
shared cross-machine telemetry store."""
self._identity = _Identity(account=account or "", role=role or "",
machine=machine or "", shared_dir=shared_dir or "")
def record(self, kind: str, name: str, ok: bool, detail: str = "",
agent_role: str = "") -> None:
"""Append one audit event. Never raises."""
try:
now = datetime.now()
event = CanonicalAuditEvent(
ts=now.isoformat(timespec="seconds"),
kind=kind,
agent_role=agent_role or "",
name=name or "",
ok=bool(ok),
detail=(detail or "")[:2000],
account=self._identity.account,
role=self._identity.role,
machine=self._identity.machine,
)
self.audit_dir.mkdir(parents=True, exist_ok=True)
path = self.audit_dir / f"{now.strftime('%Y-%m-%d')}.jsonl"
with path.open("a", encoding="utf-8") as f:
f.write(json.dumps(event.to_dict(), ensure_ascii=False) + "\n")
self._write_shared(event, now)
except Exception: # noqa: BLE001
pass
def _write_shared(self, event: CanonicalAuditEvent, now: datetime) -> None:
identity = self._identity
if not identity.shared_dir or not identity.machine:
return
try:
shared = Path(identity.shared_dir).expanduser() / "telemetry" / "audit"
shared.mkdir(parents=True, exist_ok=True)
path = shared / f"{identity.machine}-{now.strftime('%Y-%m-%d')}.jsonl"
with path.open("a", encoding="utf-8") as f:
f.write(json.dumps(event.to_dict(), ensure_ascii=False) + "\n")
except Exception: # noqa: BLE001
pass
def load_events(self, start: Optional[date] = None, end: Optional[date] = None,
kind: Optional[str] = None,
directory: Optional[Path] = None) -> List[CanonicalAuditEvent]:
"""Events between ``start``/``end`` (inclusive; None = unbounded),
optionally filtered to one ``kind``."""
directory = directory or self.audit_dir
if not directory.exists():
return []
events: List[CanonicalAuditEvent] = []
for path in sorted(directory.glob("*.jsonl")):
try:
day = datetime.strptime(path.stem, "%Y-%m-%d").date()
except ValueError:
continue
if (start and day < start) or (end and day > end):
continue
try:
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
raw = json.loads(line)
if kind is not None and raw.get("kind") != kind:
continue
events.append(CanonicalAuditEvent.from_dict(raw))
except (OSError, json.JSONDecodeError):
continue
return events