Files
DECNET/decnet/orchestrator/drivers/base.py
anti 636c057cc5 refactor(orchestrator): extract ActivityDriver ABC + driver factory
Stage 4 of the realism migration. Lifts the driver Protocol into a
proper ABC with default plant_file/read_file methods (raise
NotImplementedError), and adds get_driver_for(action) so the
orchestrator worker can dispatch by action shape without isinstance
chains.

SSHDriver now inherits ActivityDriver and implements:
- plant_file: streams base64 via stdin (ARG_MAX-safe, mirrors
  decnet.canary.planter; commit c17b9e0). Honours mtime via touch -d
  so realism-planned files don't all stamp at wall-clock-now.
- read_file: docker exec cat with FileNotFoundError on rc=1, used by
  the upcoming EditAction (stage 3b).

EmailDriver inherits ActivityDriver. Driver alias kept for back-compat
during the migration; removed once realism stages 5-7 land.
2026-04-27 16:09:46 -04:00

93 lines
3.3 KiB
Python

"""Driver ABC for orchestrator actions.
Each concrete driver (SSH, Email, future HTTP/SMB/MySQL) maps one
:class:`~decnet.orchestrator.scheduler.Action` shape to a side effect
on a target decky and returns an :class:`ActivityResult` the
orchestrator persists.
The ABC lives here, the dispatch factory in
:mod:`decnet.orchestrator.drivers` ``__init__``, and the impls in
sibling modules — same pattern as :mod:`decnet.canary.factory`,
:mod:`decnet.web.db.factory`, and :mod:`decnet.bus.factory`.
Why ABC and not :class:`Protocol`: drivers also expose lower-level
helpers (``plant_file``, ``read_file``) that the planner-driven
realism path will call directly without going through ``run``.
Inheritance pins the contract for those helpers; a structural
protocol would let a typo silently produce a half-implemented driver.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
from decnet.orchestrator.scheduler import Action
@dataclass
class ActivityResult:
"""Outcome of one driver invocation.
``payload`` is the per-action JSON envelope the worker writes to
the ``OrchestratorEvent.payload`` column and to the bus event
body.
"""
success: bool
payload: dict[str, Any] = field(default_factory=dict)
class ActivityDriver(ABC):
"""Base class every concrete orchestrator driver inherits.
Subclasses MUST implement :meth:`run` — the action-shape dispatch.
Subclasses that interact with files on the target decky SHOULD
implement :meth:`plant_file` and :meth:`read_file` so the realism
edit-in-place path can read existing artifacts before mutating
them. Drivers that don't touch files (e.g. a future pure-traffic
driver) raise :class:`NotImplementedError` from those, and the
planner avoids picking ``EditAction`` for them.
"""
@abstractmethod
async def run(self, action: Action) -> ActivityResult:
"""Execute the action against its target decky."""
async def plant_file(
self,
decky_name: str,
path: str,
content: bytes,
*,
mode: int = 0o600,
mtime: datetime | None = None,
) -> ActivityResult:
"""Write *content* to *path* inside *decky_name*.
Default raises :class:`NotImplementedError`; concrete drivers
that have a write transport (docker exec, ssh, etc.) override.
Bytes-typed so binary artifacts (DOCX/PDF) survive the wire.
"""
raise NotImplementedError(
f"{type(self).__name__} does not support plant_file"
)
async def read_file(self, decky_name: str, path: str) -> bytes:
"""Read *path* from inside *decky_name*.
Required for the realism edit-in-place flow (stage 3b of the
realism migration): the driver reads the previous body, the
realism engine produces the next iteration, the driver writes
it back. Default raises :class:`NotImplementedError`.
"""
raise NotImplementedError(
f"{type(self).__name__} does not support read_file"
)
# Back-compat alias so existing imports of ``Driver`` keep working
# while consumers transition to ``ActivityDriver``. Removed once the
# realism migration is complete.
Driver = ActivityDriver