"""Configuration for the WriteGuard middleware. The WriteGuard middleware intercepts external-write tool calls so they can be recorded for human approval **before** the underlying tool executes. It is activated per-run via a ``maf_run_id`` flag in the run context, so legacy single-agent runs are completely unaffected (the middleware no-ops). This module is harness-only: it defines the config schema and the ``WriteGuardPolicy`` / ``WriteGuardInterceptor`` Protocols. The concrete implementations live in the application layer (``app.maf_roundtable``) and are resolved at runtime via ``resolve_variable`` from the ``use`` dotted path. """ from __future__ import annotations from typing import Any, Protocol, runtime_checkable from pydantic import BaseModel, Field class WriteGuardProviderConfig(BaseModel): """Configuration for the application-layer WriteGuard implementations. ``policy_use`` resolves to a callable/class implementing ``WriteGuardPolicy``; ``interceptor_use`` resolves to one implementing ``WriteGuardInterceptor``. Both are resolved lazily by the harness via ``deerflow.reflection.resolve_variable`` so the harness never imports ``app.*``. """ policy_use: str = Field( description="Dotted path to the WriteGuardPolicy factory, e.g. 'app.maf_roundtable.write_guard_bridge:make_policy'" ) interceptor_use: str = Field( description="Dotted path to the WriteGuardInterceptor factory, e.g. 'app.maf_roundtable.write_guard_bridge:make_interceptor'" ) config: dict = Field(default_factory=dict, description="Implementation-specific settings passed as kwargs") class WriteGuardConfig(BaseModel): """Configuration for the WriteGuard middleware. When ``enabled`` and a ``provider`` is configured, the harness appends a ``WriteGuardMiddleware`` to the runtime middleware chain. The middleware itself only acts when the run context carries a ``maf_run_id`` flag — every other run is a pass-through. """ enabled: bool = Field(default=False, description="Enable the WriteGuard middleware in the chain") provider: WriteGuardProviderConfig | None = Field( default=None, description="Application-layer policy/interceptor factories" ) _write_guard_config: WriteGuardConfig | None = None def get_write_guard_config() -> WriteGuardConfig: """Get the write-guard config, returning defaults if not loaded.""" global _write_guard_config if _write_guard_config is None: _write_guard_config = WriteGuardConfig() return _write_guard_config def load_write_guard_config_from_dict(data: dict) -> WriteGuardConfig: """Load write-guard config from a dict (called during AppConfig loading).""" global _write_guard_config _write_guard_config = WriteGuardConfig.model_validate(data) return _write_guard_config def reset_write_guard_config() -> None: """Reset the cached config instance. Used in tests to prevent singleton leaks.""" global _write_guard_config _write_guard_config = None # ── Protocols (harness ↔ application contract) ───────────────────────────── @runtime_checkable class WriteGuardPolicy(Protocol): """Decides whether a given tool call must be intercepted for approval. Implementations are constructed by the application layer and passed into the harness ``WriteGuardMiddleware``. They must NOT import deerflow.* (the harness owns this Protocol definition). """ def should_intercept(self, tool_name: str, args: Any, run_context: dict | None = None) -> bool: # noqa: ANN401 """Return True if the tool call must be recorded for approval before execution. ``run_context`` (the shared run-context dict) lets the policy distinguish reads from writes using the frozen capability snapshot + write policy, so read tools are never intercepted (they pass straight through, including on the sync path) while writes are gated by the active write policy. """ ... @runtime_checkable class WriteGuardInterceptor(Protocol): """Records an intercepted write tool call (action + approval + audit event). The interceptor is responsible for the full create-pending-action flow (see ``store.create_pending_action_bundle``). It receives the run context dict (the same one ``request.runtime.context`` exposes) so it can resolve the current ``session_id`` / ``run_id`` / ``agent_id`` / ``turn_index`` and the write policy. It returns a dict describing the interception outcome so the middleware can build the ToolMessage the model sees in place of the real tool result. Expected return shape:: { "status": "approval_required" | "allowed", "action_id": "", "approval_id": "", "tool_name": "", } """ async def intercept( self, *, tool_name: str, args: Any, # noqa: ANN401 tool_call_id: str, run_context: dict, ) -> dict[str, Any]: """Record the tool call for approval. Never executes the real tool.""" ... async def validate_approved_action( self, *, tool_name: str, run_context: dict, ) -> bool: """Validate a post-approval deterministic execution before it runs. Called on the async path when the run context binds ``maf_approved_tool`` + ``maf_approved_action_id``. Must query the stored action and confirm: its status is ``running`` (the executor already occupied it), it belongs to the current ``run_id`` / ``session_id`` / ``agent_id``, and its ``tool_name`` matches. Returns True only when all hold — the middleware fails closed (blocks the call) otherwise. Field-existence alone is NOT sufficient. """ ...