Skip to content

Architecture diagrams

These diagrams describe the immutable v0.3.0 production baseline (same active READ path as v0.2.2) and the inert Tier 1 development framework it ships. Solid paths are active in production; future/dormant paths are explicitly labeled.

Overall architecture

flowchart LR
    Caller[Trusted local MCP client] -->|stdio| App[Application / FastMCP]
    App --> Registry[ToolRegistry]
    Registry --> Tools[42 thin READ tools]
    Tools --> Domain[PfSenseClient]
    Domain --> Rest[RestApiClient\nGET-only]
    Rest --> Transport[HttpTransport]
    Transport -->|HTTPS GET| PfSense[pfSense REST API]
    Rest --> Models[Typed Pydantic models]
    Models --> Tools

    Profile[Auditor capability profile] --> Registry
    Endpoints[Verified endpoint registry] --> Rest
    Mock[MockTransport] -. offline tests .-> Rest

    Tier0[Dormant Tier 0 WRITE modules] -. not constructed .-> App

READ request flow

sequenceDiagram
    participant C as MCP client
    participant T as MCP tool
    participant P as PfSenseClient
    participant R as RestApiClient
    participant H as HttpTransport
    participant F as pfSense

    C->>T: tool(arguments)
    T->>T: bind/audit disclosure choice
    T->>P: semantic READ method
    P->>P: validate bounded parameters
    P->>R: get(verified endpoint, params)
    R->>R: enforce API version and GET
    R->>H: request("GET", path)
    H->>F: HTTPS GET + API-key header
    F-->>H: HTTP response
    H-->>R: TransportResponse
    R->>R: parse JSON and map status/errors
    R-->>P: untrusted dict
    P->>P: shape checks + typed model mapping
    P-->>T: model/list
    T-->>C: serialized typed output

MCP tool registration

flowchart TD
    Start[Application bootstrap] --> ProfileName[Load PFSENSE_PROFILE]
    ProfileName --> Profiles[get_profile]
    Profiles --> CapSet[Immutable capability set]
    CapSet --> Registry[ToolRegistry.register_all]

    Registry --> C1{Capability present?}
    C1 -->|yes| Build[Build thin tool callable]
    C1 -->|no| Skip[Do not register]
    Build --> Audit[Wrap with audit_logged]
    Audit --> MCP[FastMCP.tool registration]

    Registry --> WriteDispatch[register_all_write]
    WriteDispatch --> Empty[No branches / no WRITE registration]

    Tests[Registry and profile tests] -. assert 42 READ / 0 WRITE .-> MCP

Configuration loading

flowchart TD
    Env[Process environment] --> Required{Required values present?}
    Required -->|no| Fail[Sanitized ConfigurationError\nprocess exits closed]
    Required -->|yes| URL[Validate HTTPS origin]
    URL --> Identity[Validate bounded identity]
    Identity --> TLS[Validate TLS mode and CA file]
    TLS --> Logs[Validate log bounds]
    Logs --> KeyOpen[Open key descriptor\nO_NOFOLLOW]
    KeyOpen --> KeyChecks{fstat: regular, current owner,\nno group/other bits, bounded?}
    KeyChecks -->|no| Fail
    KeyChecks -->|yes| KeyRead[Read bounded first line\nfrom same descriptor]
    KeyRead -->|empty/control/too long/read failure| Fail
    KeyRead --> Config[Immutable PfSenseConfig]
    Config --> SecretFilter[Register key with log redaction]
    SecretFilter --> Factory[Construct transport and READ client]

REST client and error boundary

flowchart LR
    Domain[PfSenseClient] --> Endpoint[EndpointInfo]
    Endpoint --> Version{API version supported?}
    Version -->|no| Unsupported[UnsupportedOperationError]
    Version -->|yes| Method[GET-only request path]
    Method --> Transport[Transport.request]
    Transport --> Response{Outcome}
    Response -->|connection/timeout| Connection[PfSenseConnectionError]
    Response -->|401/403| Auth[PfSenseAuthError\nsanitary identity-free message]
    Response -->|other failure| API[PfSenseApiError\nstatus + response ID only]
    Response -->|success| JSON[Parse JSON object]
    JSON --> Shape[Domain shape/model validation]
    Shape -->|invalid| ShapeError[PfSenseResponseShapeError]
    Shape -->|valid| Public[Typed public model]

Audit logging

flowchart TD
    Invoke[Tool invocation] --> Binder[Signature binding]
    Binder --> Context[tool name\nupstream identity\nmetadata supported/requested]
    Context --> Start[tool_invoked]
    Start --> Call[Execute tool]
    Call -->|success| Success[tool_succeeded\nduration + no values]
    Call -->|PfSenseMCPError| Domain[tool_failed\nfailure_class=domain\nexception class only]
    Call -->|other Exception| Unexpected[tool_failed\nfailure_class=unexpected\nexception class only]
    Domain --> Reraise[Re-raise unchanged]
    Unexpected --> Reraise

    Filter[SecretRedactionFilter] --> Sink[Rotating owner-only local log]
    Start --> Filter
    Success --> Filter
    Domain --> Filter
    Unexpected --> Filter

    Never[Arguments, responses, payloads,\nexception messages, credentials] -. never logged .-> Sink

Security boundaries

flowchart LR
    subgraph TrustedLocal[Trusted local account boundary]
        Client[MCP client]
        Server[stdio MCP server]
        Key[Owner-only API-key file]
        Logs[Value-minimized logs]
        Client --> Server
        Key --> Server
        Server --> Logs
    end

    subgraph UntrustedInput[Untrusted data boundary]
        Appliance[pfSense API]
        JSON[Raw JSON / HTTP status]
        Appliance --> JSON
    end

    Server -->|HTTPS GET| Appliance
    JSON --> Validators[Status + shape + Pydantic validation]
    Validators --> Public[Credential-free public schemas/output]
    Public --> Server

    Repo[Public repository / CI] -->|MockTransport + sanitized fixtures only| Server
    Private[Private live acceptance] -. separately approved .-> Appliance

Inert Tier 1 framework and future execution path

flowchart TD
    Caller[Separately authorized operator] --> Prepare[Prepare mutation intent]
    Prepare --> Read[Existing verified READ method]
    Read --> Snapshot[Capture target pre-state]
    Snapshot --> Store[Authoritative crash-safe contract store]
    Store --> Contract[Target/capability/endpoint/method/intent-bound contract ID]

    Caller --> DryRun[Dry-run tool]
    DryRun --> Validate[Allow-list + capability + typed payload validation]
    Validate --> Diff[Predicted semantic diff\nzero mutation]

    Caller --> Confirm[Explicit execute confirmation + contract ID]
    Confirm --> Load[Load authoritative contract by ID]
    Load --> CAS[Atomic PREPARED → EXECUTING]
    CAS -. executor not implemented .-> Write[Future WriteApiClient\nonly non-GET chokepoint]
    Write -->|approved HTTPS mutation| PfSense[Disposable/test pfSense first]
    PfSense --> ReadBack[Verified READ-back]
    ReadBack --> Commit{Expected semantic state?}
    Commit -->|yes| Verified[VERIFIED]
    Commit -->|no/ambiguous| Unknown[RECONCILIATION\noperator decision required]

    Verified --> Rollback[Target-bound rollback]
    Rollback --> RollCAS[VERIFIED → ROLLING_BACK]
    RollCAS --> Restore[Approved restore request]
    Restore --> VerifyRestore[READ-back semantic equivalence]
    VerifyRestore --> RolledBack[ROLLED_BACK]

    Policy[Empty production mutation policy] -. blocks .-> Write
    Warning[No executor, endpoint, capability, or tool is active] -.-> Write

The contract, state-machine, policy, audit, and authenticated-store boxes exist only as isolated domain infrastructure. The dotted execution path requires every remaining milestone in the Tier 1 roadmap and separate capability-specific authorization before any WRITE endpoint or tool is added.