Releases: night4me/pfsense-mcp-server
Release list
v1.1.0
Defense-in-depth and onboarding release. Public MCP tool contract is
unchanged from 1.0.0 (95 pfSense READ tools + 2 documentation guidance
tools, 0 default-reachable WRITE = 97 total) — this release is not a
capability expansion.
The headline addition is managed READ-only credential provisioning: a
dedicated, project-provisioned pfsense-mcp-readonly pfSense service
account holding exactly the 94 READ privileges this project documents —
live-LAB-verified to receive HTTP 403 AUTH_AUTHORIZATION_FAILED when used
directly against this project's own reviewed WRITE endpoint, i.e. rejected
by pfSense itself, not only by this server's tool surface. This is the
recommended path for new read_only setups via the setup wizard;
bring-your-own-key remains fully supported, unchanged by default, for
existing installations.
Added
- Managed READ-only service-account provisioning — a fixed, dedicated
pfsense-mcp-readonlyidentity, entirely separate namespace/journal/
lock/custody state from thewrite_protectedpfsense-mcpaccount.
Reachable viapfsense-mcp-security bootstrap --target-profile read_onlyandpfsense-mcp-security recover --target-profile read_only. - Setup-wizard integration for managed READ-only provisioning. A new
"Account" wizard step (read_only postures only) offers a dedicated
least-privilege account [Recommended] alongside the existing
bring-your-own-key option. The managed-vs-BYOK choice is security-bound
into the setup plan, its digest, and the apply-time confirmation token —
a confirmed BYOK plan can never be substituted for a managed-provisioning
apply, or vice versa. --target-profile {write_protected,read_only}flag on
bootstrap/recover— explicit, additive; defaults to
write_protected(unchanged behavior for every existing invocation).
Fixed
- Recovery cross-profile isolation. Inline recovery inspection now
threads the correct target profile explicitly from the caller that
determined which bootstrap composition failed, instead of always
inspecting thewrite_protectedaccount's journal. - Operation-journal account/profile allowlist is now a closed,
hardcoded pair set — both fields must still match together; no other
account/profile combination can ever create a journal record. docs/PFSENSE_LEAST_PRIVILEGE_MATRIX.mdstaleness (understated the
verified pfSense-pkg-RESTAPI version range).
Changed
- Verified
pfSense-pkg-RESTAPIversion range extended tov2.10.2
(fromv2.10.0), re-verified directly against pinned source at both
v2.10.1andv2.10.2. - MCP tool-surface architecture reviewed and retained. An empirical
benchmark found no measurable selection-accuracy problem with the
current 97-tool explicit surface. Two non-generic alternatives were
designed and benchmarked but not adopted — no production architecture
change was made.
Full detail: CHANGELOG.md.
v1.0.0
First stable release. Public MCP tool contract is unchanged from 0.9.0
(95 pfSense READ tools + 2 documentation guidance tools, 0
default-reachable WRITE = 97 total) — this release is a product-maturity
and correctness pass, not a capability expansion. See
docs/STABILITY.md
for the version-independent stability promise now made across the
MCP/CLI/config/persisted-state surfaces.
Fixed
serverInfo.versionreported the installedmcpSDK's own version
instead of this package's version in every MCPinitialize
response —FastMCPnever forwards aversion=to the low-level
mcp.server.lowlevel.Serverit constructs internally, so an unset
.versionfell back to themcppackage's ownimportlib.metadata
version. Now set explicitly from one shared resolver.setup write-client-configfailed on a genuinely clean$HOME
with no pre-existing~/.codexdirectory, despite a valid
confirmation token — a missing parent directory is now created
automatically and safely (symlink-safe, ownership-checked,
owner-only permissions).- Setup wizard guidance text lost whitespace at wrap boundaries on
narrow terminals — affected paragraphs rebuilt as continuous strings
wrapped fresh at render time. - TLS certificate verification failures were reported with one
generic message, concealing whether the failure was an untrusted
certificate authority or a certificate valid for a different
hostname/IP than the one connected to. Now distinguishes CA-trust
failure, hostname/IP identity mismatch, and expired/not-yet-valid
certificates using structuredssl.SSLCertVerificationError
evidence — never string-guessing, never suggesting an insecure
bypass. - A successful
read_onlysetup applyended with an unexplained
Doctor ready: False—doctor's checks are exclusively about the
optional hardware-witness ceremony, but were run and reported
unconditionally regardless of the selected anchor. Now posture-aware.
Changed
SECURITY.md's "Supported versions" section, frozen at the
v0.2.x/v0.3.0era, replaced with a version-independent policy.- Corrected two client-integration-guide claims found stale by live
verification against current vendor documentation. Development Statusclassifier updated from4 - Betato
5 - Production/Stable.
Full detail: CHANGELOG.md.
v0.9.0
Adds a second, structurally distinct MCP guidance tool covering the community-maintained pfREST package -- public MCP contract: 95 pfSense READ tools + 2 documentation guidance tools (was 1), 0 default-reachable WRITE = 97 total (was 96).
Added
pfsense_get_api_guidance-- a new guidance tool coveringpfSense-pkg-RESTAPI(documented at pfrest.org), never Netgate product documentation, never blended with the existingpfsense_get_official_guidance. Four bounded query modes --tool,endpoint,model,topic-- returning cross-source evidence explicitly labeled by provenance:PROJECT_AUTHORED(this project's own tool interpretation),PFREST_UPSTREAM(pfrest.org's live documentation, fetched through a narrow, allowlisted, HTTPS-only, GET-only fetcher with a hard response-size cap and a bounded cache), andLIVE_APPLIANCE_SCHEMA(your connected appliance's own/api/v2/schema/openapi, fetched through the same authenticated transport every READ tool already uses). When sources disagree, the disagreement is stated explicitly -- never silently merged, never silently dropped.- Project-authored tool guidance (
pfsense_mcp.guidance.tool_guidance) is now wired intopfsense_get_api_guidance'stoolquery mode. make pfrest-privilege-crosscheck-- an offline, advisory, maintainer-facing script comparing what pfrest.org and (if configured) your connected appliance each declare as a READ tool's required pfSense privilege. Not part of the MCP surface; never grants a privilege or modifies configuration.make pfrest-schema-diff-- an offline, advisory, maintainer-facing semantic (not byte-level) OpenAPI comparison across twelve dimensions (paths/methods, operationIds, parameters, schemas, fields, enums, default values, required packages, auth metadata, allowed privileges, applies-immediately metadata, extensions, version metadata), classifying differences without assuming a cause. Not part of the MCP surface.
Safety
Documentation and guidance content -- from any of the four provenance sources -- is evidence, never authorization. It cannot select, influence, or execute any action this server takes; it cannot reach a WRITE capability; and no source can masquerade as another. This is enforced structurally (isolation tests, no authorization-shaped output fields) and verified adversarially, including live against a real appliance, before this release.
Not included in this release: a persistent pfREST documentation cache, conditional-GET revalidation, a model-name discovery tool, or a production CE-vs-Plus schema comparison (the tooling to run one exists offline, but no comparison was performed against production).
Full detail: CHANGELOG.md, docs/adr/ADR-035-pfrest-live-guidance-layer.md, and docs/ACCEPTANCE_v0.9.0.md.
v0.8.0
CLI-only expansion of the pfsense-mcp-security operator tooling (ADR-021/ADR-033). No MCP capability change: the public MCP tool contract remains byte-identical to v0.7.2 -- still 95 pfSense READ tools + 1 official-guidance tool, 0 default-reachable WRITE. None of this release's new functionality is reachable from, or wired into, pfsense_mcp.server/the MCP tool registry/normal application startup; it is exclusively the separate pfsense-mcp-security CLI entry point.
Added
pfsense-mcp-security setup-- a guided, non-mutating discovery + plan-only wizard (interactive by default, or--non-interactivefor deterministic automation), plussetup apply, a wholly separate, explicit command that can apply a generated plan:read_onlyperforms one read-only connectivity check;write_protectedcomposes the existingbootstraporchestration to provision the one fixed ADR-033 service account. Includes inlineRECOVERY_REQUIREDdelegation,PFSENSE_SETUP_APPLY_CONFIRM_TOKENfor CI-friendly non-interactive confirmation, andsetup write-client-config(merge-only MCP client config writing, gated behind its own explicit--confirm).pfsense-mcp-security recover-- standalone ADR-033 recovery-execution orchestration. Read-only inspection by default; execution requires both--execute <ACTION>and the exact incident-bound confirmation token.
Fixed
bootstrap(andsetup apply --capability-posture write_protected) restart classification -- a prior operation journal is no longer unconditionally treated as requiring recovery attention on every subsequent run. One fresh, read-only, GET-only live observation is now attempted first; only an exact match against every expected binding field resolves to a clean, already-complete restart. A strictly fail-closed correctness improvement, not a relaxation.
Full detail: CHANGELOG.md and docs/ACCEPTANCE_v0.8.0.md.
v0.7.2
Tier 1 correctness fix and validation-pipeline improvement. No MCP
capability change. Public contract byte-identical to v0.7.1: 95
pfSense READ tools + 1 official-guidance tool, 0 default-reachable
WRITE, confirmed by a tests/contracts/mcp_public_contract_v0.7.2.json
snapshot that diffs to zero against v0.7.1's own snapshot.
Fixed
MutationExecutorreal-wall-clock expiry check —execute()
calledRecoveryContract.is_expired()with no explicitnow=
argument, so it silently fell through to real
datetime.now(timezone.utc)regardless of any deterministic clock
the surrounding test/store already used. Under a long-running test
suite this occasionally caused a 4-minute contract TTL to expire
before the two real-executor tests in
test_alias_description_execution.pyreached it, producing an
intermittentContractConflictErrorunrelated to any real
authorization problem. Fixed with the same constructor-injectable
Clockseam already used bySqliteRecoveryContractStoreand
SqliteAuthorizationConsumptionStore—MutationExecutornow
defaults to real UTC wall-clock time exactly as before (the only
production construction site,production_runtime.py, does not
inject a clock) and fails closed on a naive or non-UTC value. No
expiry semantics, TTL values, authorization logic, or state-machine
transitions changed.- README.md's Mermaid diagrams rendering as raw source text on
PyPI — GitHub renders fenced```mermaidblocks natively;
PyPI'sreadme_rendererdoes not, so the live PyPI project page
showed the raw diagram source as a code block. Both diagrams
(READ trust path, WRITE authorization path) replaced with
pre-rendered SVG images (assets/diagrams/*.svg), referenced by an absolute
raw.githubusercontent.comURL since PyPI has no file tree for a
relative path to resolve against. The Mermaid source remains the
maintained source of truth inassets/diagrams/*.mmd. See
docs/adr/ADR-034-mermaid-pypi-compatibility.md. - GitHub Pages documentation staleness — the published docs site
was last deployed from a commit 92 commits behindmain(still
describing a 42-tool,v0.3.x-era state). Redeployed from current
main. Added a read-only staleness detector
(scripts/docs_pages_freshness_check.py, weekly + on-docs-push CI
check) that cannot itself deploy anything — automatic deployment on
every docs change remains a separate, explicit owner decision.
Added
pfsense-mcp-security bootstrapCLI subcommand (ADR-033 CLI
Integration Slice 3) — a journal-aware, locking bootstrap-
orchestration layer composing the already-implemented ADR-033
security-bootstrap primitives into one operator-facing command. This
is a CLI-only administrative tool, invoked outside the MCP server
process; it registers no MCP tool, adds nothing to the public MCP
contract, and does not change READ or WRITE reachability.
Changed
- Validation-pipeline performance: the full offline pytest suite
now runs underpytest-xdist(-n 6 --dist=loadscope), cutting
local wall-clock time from ~146s to ~65s (~55%) with zero test
coverage loss — two tests that cannot safely collect under xdist
(collection-time random parametrize values; a test asserting a
fresh, untouchedsys.modulesstate) run in a small serial pass
instead.make quickandmake validateare correspondingly
faster (~75s and ~77s). Also removed a genuine redundancy from CI:
thetestjob ran the full suite twice in the same job/SHA (a
standalonepytest -qstep immediately followed bymake quick's
own pytest stage) — the standalone step was removed.
v0.7.1
Documentation/packaging presentation correction. NO functional MCP/API/
security change. Public contract byte-identical to v0.7.0: 95
pfSense READ tools + 1 official-guidance tool, 0 default-reachable
WRITE, confirmed by a tests/contracts/mcp_public_contract_v0.7.1.json
snapshot that diffs to zero against v0.7.0's own snapshot.
Fixes a documentation/release-integrity defect found after v0.7.0
published: README.md's Quick start still instructed
pip install 'pfsense-mcp-server==0.5.1' — stale across two releases,
and, because hatchling embeds README.md verbatim as the wheel/sdist
long_description at build time, permanently frozen into the
already-published, immutable v0.7.0 PyPI artifact (PyPI's rendered
project page cannot be edited in place — only a new release changes
what it shows). This release exists solely to publish the correction.
Fixed
- README.md's Quick start install command — replaced the stale
pinned==0.5.1with an unpinnedpip install --upgrade pfsense-mcp-server. No other install instruction in this repository
pins an exact release version, and this exact line had already gone
stale twice in a row (throughv0.6.0andv0.7.0) — evidence a
pinned Quick start version is a recurring maintenance liability, not
a deliberate convention. - Stale current-state documentation, found during the same
repository-wide sweep:docs/ROADMAP.md's "Current baseline" section
still namedv0.5.1/84 tools as current (three releases stale);
docs/index.md's two tool-count lines omitted the guidance tool;
three spots indocs/THREAT_MODEL.md(an assets-table row, a section
heading, and its intro sentence) still described the official-
guidance layer as inert/consumerless, missed byv0.7.0's own
release-readiness audit. Fixed narrowly; historical CHANGELOG,
ACCEPTANCE_v0.*.md, dated compatibility-evidence rows, and other
release-immutable records were left untouched. - Regression protection added:
tests/test_readme_install_version.py
asserts any pinned version in README.md'spip installline for this
package matchespyproject.toml's current version, so a stale pin
cannot silently recur.
v0.7.0
First official-guidance MCP tool. Public contract: 95 pfSense READ
tools (unchanged) + 1 new official-guidance tool
(pfsense_get_official_guidance), 0 default-reachable WRITE (unchanged).
The guidance tool is not a 96th pfSense READ capability — it is a
separate, structurally distinct tool class (tool_class: "guidance" in
the public contract, capability: null), not gated by the
Capability/privilege/profile system that governs appliance access, and
does not add to, widen, or reinterpret the 95-tool READ surface, the 94
distinct READ privileges, or the 1 implemented-but-default-unreachable
WRITE tool — all three are byte-identical to v0.6.0.
pfsense_get_official_guidance(capability) returns project-authored
summaries of official Netgate/pfSense documentation for a given
pfsense-mcp-server capability, from a deterministic, Git-tracked,
PR-reviewed bundled registry (ADR-017/ADR-018) — never a live web
fetch, never a runtime documentation retrieval of any kind. Every
returned entry carries structural provenance: a canonical Netgate
source URL, an evidence level, and an applicability state
(APPLICABLE/PARTIALLY_APPLICABLE/VERSION_UNCONFIRMED/
EDITION_MISMATCH/STALE/NO_OFFICIAL_GUIDANCE_FOUND) computed from
the pfSense appliance's own observed edition/version — resolved by the
tool itself through the exact same already-authenticated read path
every other READ tool uses (GET /api/v2/system/version), never
supplied by the caller and never guessed. Guidance output is fixed-schema
data only: it carries no field of type capability, endpoint, method, or
confirmation token, cannot invoke a tool, alter a capability, or
authorize any action, and is returned as inert content even when a
registry entry is adversarial. A fixed, non-overridable disclaimer field
on every response states plainly that guidance is documentation, not
observed live appliance state, and confers no authorization.
Fixed
- Server-startup failure-coupling, found during this release's own
release-readiness audit. An eager import in
pfsense_mcp/guidance/__init__.pymeant that importing
GuidanceReferencealone (needed for the new tool's own Pydantic
schema) was enough to trigger the guidance registry's load-time
integrity self-check on the server-startup path for every profile
with any capability granted — a corrupted guidance registry entry
could have crashed the entire MCP server, taking all 95 READ tools
down with it. Fixed with a PEP 562 lazy__getattr__, verified by a
fresh-subprocess test; a corrupted registry entry now fails only that
one tool's own calls, never server startup or any other tool. - Stale "guidance layer unwired"/"95 tools" claims, found during the
same audit, acrossREADME.md,docs/API.md,
docs/CONFIGURATION.md,docs/SECURITY_MODEL.md,
docs/THREAT_MODEL.md(scope line, TB9, the guidance adversarial-paths
table, and the A7 threat entry),docs/OFFICIAL_GUIDANCE_LAYER.md,
docs/VERSION_AWARE_GUIDANCE.md,docs/adr/ADR-017,
docs/adr/ADR-018, the ADR index, and all 6 client example guides.
Fixed narrowly and additively; historical CHANGELOG/v0.6.0-specific
records were left untouched.
v0.6.0
READ-capability expansion. Public contract: 84 → 95 READ tools
(0 default WRITE, unchanged). Useful READ coverage: 84/105 (80.0%) →
95/105 (90.5%) against this project's own capability-audit denominator
(unchanged — every candidate added this release was already counted in
that original 105, none legitimately expands it). 84 → 94 distinct READ
privileges; 85 → 95 write-protected combined privileges. Every new tool
was exercised against a real pfSense instance (LAB, this project's
disposable CE 2.9.0 appliance) before public registration — 10 of the
11 with an exact field-level key-set match against their typed model
(FIELD_MODEL_LIVE_VERIFIED); the remaining one (WireGuard tunnel
addresses) confirmed reachable and correctly authorized but returned an
empty list on this LAB (ENDPOINT_VERIFIED), so its field shape rests
on independent schema/security review rather than an observed
populated item. No package was installed to support this release; the
one package-conditional addition (WireGuard tunnel addresses) uses a
package already present on the LAB from prior, separately authorized
work.
Added
-
Schema field-drift regression protection (v0.6.0 Phase B, Batch A).
scripts/lib/schema_drift.pyprovides a general mechanism,
independently designed (not derived from the comparison project
investigated in the v0.6.0 competitive audit), that asserts every
field a pinned upstream OpenAPI schema component declares is either a
field this project's Pydantic response model already declares, or is
present in an explicit, reviewedintentional_exclusionsallowlist
(e.g.WireGuardPeerStatus.preshared_key, deliberately never
modeled). This closes a real, previously-unguarded gap: a future
pfREST release adding a field to an already-modeled response object
would otherwise go completely unnoticed, since a Pydantic model
silently ignores unknown upstream keys by construction.
tests/test_schema_field_drift.pyregisters four already-shipped
models (ConfigHistoryRevision,SystemTimezone,
WireGuardTunnelStatus,WireGuardPeerStatus) against a small,
explicitly curated fixture (tests/fixtures/pinned_response_schemas.json)
and proves the mechanism itself fires correctly against synthetic
ordinary-field, secret-like-field, nested-model, exclusion-allowlist,
stale-exclusion, and schema-evolution (nullable) cases. No MCP tool,
capability, or public contract change. Public contract remains 84
READ / 0 default WRITE. -
pfsense_get_diagnostics_config_history_revisions(v0.6.0 Phase B,
Batch B). Lists configuration-history (backup) revisions: change
timestamp, pfSense's own system-generated audit description, the
pfSense version at the time, and the backup file size. Metadata only
— the v0.6.0 Phase A qualification independently confirmed, against
the upstreamConfigHistoryRevision.incModel source (not just the
OpenAPI schema), that this endpoint's response never includes the
backup's actual configuration content, only filesystem-level metadata.
The underlying client method, typed model, andEndpointsentry
already existed (added 2026-08-16 for internal ADR-026 evidence
gathering,verified=Truefrom that session's real LAB call); this
release adds only the public MCP tool registration. A fresh
confirmatory LAB call was attempted this session but could not be
completed (the read-only LAB service account's privilege scope did
not yet include this endpoint, and granting it required admin LAB
access not available in this session) — promotion rests on the
pre-existing 2026-08-16 evidence, disclosed explicitly rather than
overstated; seedocs/PFSENSE_LEAST_PRIVILEGE_MATRIX.md. Public
contract: 84 → 85 READ tools (0 default WRITE, unchanged). -
LogSettingsmodel + client method, implemented and offline-tested
(v0.6.0 Phase B, Batch C — not yet registered). New
src/pfsense_mcp/models/log_settings.py(34 fields, all
boolean/string/integer, nowriteOnly/secret-shaped field anywhere —
re-verified directly against the pinned schema immediately before
implementation, independent of the v0.6.0 Phase A finding) and
PfSenseClient.get_status_logs_settings(). LAB verification could not
be completed this session: the read-only LAB service account's
privilege scope covers only already-registered tools, and granting
api-v2-status-logs-settings-getrequired admin LAB access that did
not authenticate successfully.Endpoints.STATUS_LOGS_SETTINGSremains
verified=False; notools/read/file,Capabilityenum member, or
registry wiring was added, matching this project's established
WireGuard-package-blocker precedent. Public contract unchanged at 85
READ / 0 default WRITE. Registered in the Batch A schema-drift
registry. -
Apply-status sweep, implemented and offline-tested (v0.6.0 Phase B,
Batch D — not yet registered). Eight new models/client methods for
firewall/virtual_ip/apply,interface/apply,routing/apply,
services/dhcp_server/apply,services/dns_forwarder/apply,
services/dns_resolver/apply,vpn/ipsec/apply,vpn/wireguard/apply
— each independently re-verified against the pinned schema (all
trivial{"applied": bool}shapes;interface/applyadditionally
has a flatpending_interfaces: list[str]; no secret material in
any). Mirrors the already-shippedFirewallApplyStatuspattern.
applied/pending_interfacesare modeled| None(schema-declared
nullable: true), deliberately not assumed non-null the way the
pre-existingFirewallApplyStatusis, since no live call has
confirmed any of these eight endpoints' actual behavior yet. None
registered as public tools — same LAB-access blocker as Batch C.
Public contract unchanged at 85 READ / 0 default WRITE. -
WireGuard tunnel addresses, implemented and offline-tested (v0.6.0
Phase B, Batch E — not yet registered). New
WireGuardTunnelAddressmodel/client method for
vpn/wireguard/tunnel/addresses(address/mask/descr, none
writeOnly, no secret material —address/maskredacted by
default, matchingRoutingStaticRoute's established convention).
Independently re-confirmed NOT redundant with the already-shipped
WireGuardTunnelStatus, which has no address/CIDR field at all —
unlike WireGuard peer allowed-IPs, deliberately not implemented
since it is already nested asWireGuardPeerStatus.allowed_ips.
pfSense-pkg-WireGuard is already installed on the LAB (prior,
separately authorized action), so package availability is not this
batch's blocker — the same read-only LAB service-account privilege
scope issue as Batches C/D is. Not registered as a public tool. Public
contract unchanged at 85 READ / 0 default WRITE. -
v0.6.0 Phase B completion: LAB-verified and registered all 10
previously-pending Batch C/D/E candidates. The read-only LAB
service account (pfsense-mcp, id=2) was synced from a stale
42-privilege snapshot (dating to its original 2026-08-19 ADR-033
provisioning) to the current full requirement, after reconciling —
with direct evidence fromAI_CONTEXT.md's own provisioning
checkpoint, not assumption — that this account is the project's
single, intentionally-provisionedwrite_protected-profile service
account (hence its pre-existingapi-v2-firewall-alias-patch
privilege, preserved throughout the sync). All 10 candidates were
then live-verified in one pass:pfsense_get_status_logs_settings—FIELD_MODEL_LIVE_VERIFIED.
Fixed a real defect found by the live call, not assumed correct:
sourceipneeded the samenullable: false→Optionalwidening
already applied to 17 other fields, missed in the earlier partial
fix — a second live parse attempt (not a first-pass assumption)
caught it.pfsense_get_firewall_virtual_ip_apply_status,
pfsense_get_interface_apply_status,
pfsense_get_routing_apply_status,
pfsense_get_dhcp_server_apply_status,
pfsense_get_dns_forwarder_apply_status,
pfsense_get_dns_resolver_apply_status,
pfsense_get_ipsec_apply_status,
pfsense_get_wireguard_apply_status— all 8
FIELD_MODEL_LIVE_VERIFIED, exact key-set match, independently
confirmed per-endpoint (not inferred from one passing).pfsense_get_vpn_wireguard_tunnel_addresses—
ENDPOINT_VERIFIED(200,{"data": []}— no tunnel addresses
configured on this LAB; field safety rests on the Phase A
schema/security review, not an observed populated item).
Public contract: 85 → 95 READ tools (0 default WRITE, unchanged).
Useful READ coverage: 95/105 (unchanged historical denominator, per
the Phase A correction).tests/fixtures/pfsense_openapi_schema_trimmed.json
extended with all 10 endpoints' real captured descriptions (needed
for the security-bootstrap/privilege-derivation test suite, which
resolves privileges from this fixture). Fixed the resulting
hardcoded-count cascade across test files,docs/API.md, and
docs/PFSENSE_LEAST_PRIVILEGE_MATRIX.md.
Fixed
- Stale tool-count/coverage claims across README, docs, and examples,
found during this release's own release-readiness audit: the 84→95
increase above was reflected indocs/API.mdand
docs/PFSENSE_LEAST_PRIVILEGE_MATRIX.md(updated as part of the
READ-capability work itself) but not yet inREADME.md(Key facts,
architecture diagram labels, capability-overview table and per-category
counts, package-conditional-tools note),docs/ARCHITECTURE_DIAGRAMS.md,
docs/API_SURFACE_ARCHITECTURE.md,docs/CONFIGURATION.md,
docs/PYPI_RELEASE.md,docs/SECURITY_MODEL.md,docs/THREAT_MODEL.md,
docs/index.md, or any of the 7 client-setup guides underexamples/.
Evidence-tier distinctions were preserved rather than strengthened:
README's Plus 26.07PRODUCTION VERIFIEDrow keeps its exact "82 of
84" historical cla...
v0.5.1
Documentation-accuracy and security-communication patch. NO MCP
capability change. NO runtime security-semantic change. Public contract
remains exactly 84 READ / 0 default WRITE, byte-identical to v0.5.0
— confirmed by an unchanged tests/contracts/mcp_public_contract_v0.5.1.json
snapshot relative to v0.5.0's. Every finding below is documentation or
presentation only.
Fixed
- Post-publication documentation correction (2026-08-21): incorrect
pfSense Plus REST API packaging claim. README's compatibility
section (and the matching table indocs/ACCEPTANCE_v0.5.0.md)
claimed the REST API "ships as a built-in platform component" on
pfSense Plus rather than a separately versioned package, inferred
from its absence in the general installed-package listing
(pfsense_get_system_packages) during the v0.5.0 release audit. This
was a genuine error: a direct, targeted follow-up call to
pfsense_get_system_restapi_version— the tool actually built for
this exact question, not consulted at the time — confirms the REST
API package's own self-reported version (current_version) is
v2.10 on both pfSense CE (re-confirmed live on the CE 2.9.0 LAB)
and pfSense Plus 26.07 production, identical to the CE baseline
already documented. Further investigation found the package does
not appear as a discrete entry in the general installed-package
listing on either edition — re-confirmed directly on the CE 2.9.0
LAB (which lists only the one other package genuinely installed
there) — so this was never a CE-vs-Plus difference at all, only a
characteristic of that one endpoint on every platform tested. README
anddocs/ACCEPTANCE_v0.5.0.mdcorrected to state only
independently-verified facts, distinguishing pfSense platform
version, edition, REST API package version, and schema/API
compatibility as the separate facts they are.v0.5.0itself
(already tagged, released, and published to PyPI before this error
was found) still carries the original incorrect text in its
immutable tag/Release/PyPI artifacts — per this project's own
release policy, that historical record is not altered; this fix
applies tomainand every release from here forward. No public
contract, security, or compatibility-verification-result change: the
underlying evidence (schema match, tool-count regression results)
was always correct — only the packaging-mechanism inference was
wrong. - Package-dependency documentation was incomplete. README only
documented the two WireGuard status tools as package-conditional.
Re-derived every one of the 84 registered endpoints' schema-declared
Required packagesmetadata directly (not assumed): four more tools
(pfsense_get_acme_settings,pfsense_get_bind_settings,
pfsense_get_cron_jobs,pfsense_get_freeradius_eap) reference a
package in the schema's own metadata (pfSense-pkg-acme,
pfSense-pkg-bind,pfSense-pkg-Cron,pfSense-pkg-freeradius3
respectively) but were directly confirmed, by invoking them against
systems genuinely lacking those packages (the CE 2.9.0 LAB for all
four; the Plus 26.07 production appliance for three of the four),
to succeed regardless — these read as stored
configuration/default-settings structures, not genuinely
package-gated runtime state, unlike the WireGuard status pair (which
do 404 withMODEL_MISSING_REQUIRED_PACKAGEwhen absent). README
now documents this distinction precisely instead of implying only
WireGuard has any package reference at all. - Evidence-tier terminology overlapped. README's compatibility
matrix previously usedLIVE VERIFIEDto mean "LAB or production,"
which overlapped with the separateLAB VERIFIEDtier and made the
two indistinguishable for a reader. ReplacedLIVE VERIFIEDwith
PRODUCTION VERIFIED(production only) so all four tiers
(PRODUCTION VERIFIED/LAB VERIFIED/SUPPORTED / COMPATIBLE/
EXPECTED COMPATIBLE / UNVERIFIED) are mutually exclusive. No
evidence changed — pfSense Plus 26.07's row is unaffected in
substance, only relabeled fromLIVE VERIFIEDto the more precise
PRODUCTION VERIFIED. - pfSense Plus 25.11's classification was too strong for its
evidence. Re-evaluated rather than preserved as previously written:
the evidence behind the priorSUPPORTED / COMPATIBLEclassification
was entirely adjacent (FreeBSD-generation similarity via Netgate's
own 25.11 release notes, plus pfREST v2.10 behavior observed on
other releases) — nothing this project has directly exercised
touches a 25.11 instance in any way. Downgraded to
EXPECTED COMPATIBLE / UNVERIFIED, the tier this project's own newly
mutually-exclusive definitions assign to exactly this evidence
profile. - An unqualified "verified before promotion" claim was too
universal. README's "Key facts" bullet stated every tool is
"verified against a real pfSense instance before public
registration" without distinguishing depth. Reworded to state
precisely what is true: every one of the 84 tools was exercised at
least once and confirmed to match its typed model, but some have so
far only been observed against a valid empty/default envelope on
every system tested, not populated real data — the two are not the
same claim and the README no longer conflates them. docs/TIER1_ARCHITECTURE.mdanddocs/ARCHITECTURE_DIAGRAMS.md
were stale, describing the pre-ADR-026v0.3.0-era state ("no
mutation executor exists yet," "adapter implementation remains
blocked," "these diagrams describe the immutable v0.3.0 production
baseline") despite the first WRITE capability having been built and
independently live-verified since 2026-08-16. Added a dated
historical note toTIER1_ARCHITECTURE.md(matching
SECURITY_MODEL.md's own established correction pattern) rather than
rewriting its still-accurate generic reusable-framework content, and
updatedARCHITECTURE_DIAGRAMS.md's framing and its
"Inert Tier 1 framework and future execution path" section — which
claimed "no executor, endpoint, capability, or tool is active" — to
describe the real, current architecture instead. This inaccuracy was
independent of, and unrelated to, the pfREST packaging finding above;
found during this release's own re-reading of the authoritative
architecture sources before drafting new diagrams.
Added
- Three new Mermaid architecture diagrams, derived directly from
current source (tools/registry.py,capabilities.py,profiles.py,
tier1/execution_coordinator.py,tier1/executor.py,
tier1/state_machine.py) and accepted architecture
(ADR-026,SECURITY_MODEL.md), not from this changelog entry's own
prose:- READ security path — the exact path every one of the 84 tools
takes, compact version in README near "Why this server," full
version indocs/ARCHITECTURE_DIAGRAMS.md. - Protected WRITE authorization path — the gate-by-gate
ADR-026
flow (off-host signature → 6 fail-closed pre-execution gates →
RecoveryContract→ sealedMutationExecutor→ read-back →
verified/reconciliation), compact version in README's "Protected
WRITE architecture" section, full version with all six gates named
individually indocs/ARCHITECTURE_DIAGRAMS.md. Explicitly
distinguishesIMPLEMENTED/VERIFIED/DEFAULT-REACHABLEas
three different claims, since the one capability that exists is
the first two but never the third. - Defense in depth / trust boundaries — a single high-level
diagram indocs/ARCHITECTURE_DIAGRAMS.mdshowing which failure
class each layer actually stops, limits, constrains, or detects
(deliberately not a blanket "secure" label on any layer). Corrects
a self-caught drafting error: the TPM witness was initially
labeled "optional," which contradictsSECURITY_MODEL.md's own
statement that production WRITE activation requires it (a
software-only anchor alternative is modeled but has no implemented
backend) — fixed before this diagram was ever committed. - All four diagrams (three new plus the existing set) independently
validated withmermaid's own parser (mermaid.parse()via a
headless DOM shim) before commit; full visual/browser rendering
was not available in this environment (missing
chrome-headless-shellsystem dependency), so parser-level syntax
validation is this release's evidence tier for "renders correctly"
rather than a rendered-image comparison.
- READ security path — the exact path every one of the 84 tools
v0.5.0
Major READ capability expansion — no WRITE change, no security-model
change. Public MCP contract grows from 42 to 84 READ tools (exactly a
100% increase over the last published baseline, v0.4.2), covering
roughly 80% of the useful READ capability universe identified by this
project's own capability discovery audit (267 OpenAPI paths / 243 GET
operations reviewed; every GET given exactly one disposition — up from
roughly 40% coverage at the v0.4.2 baseline). Every tool below was
independently re-verified for secret-bearing fields before
implementation, then verified against a real pfSense instance (LAB or,
where explicitly owner-authorized, production) before public
registration — never assumed from schema alone. Closes with an
independent, adversarial release-readiness audit (security regression
sweep, CE/Plus compatibility verification including a live production
Plus 26.07 pass, packaging/fresh-install/upgrade-path verification, and
a full README restructure) — see "Security" and "Changed" below for its
findings.
Added
- 2 new READ tools (production live-verified, pre-dating the P0
backlog below), public MCP contract 42 → 44 (0 WRITE, unchanged):pfsense_get_firewall_nat_outbound_mappings— outbound NAT
mappings (source/destination/targetaddress/alias fields
redacted by default, matchingFirewallNatPortForward's
established convention;source_hash_keyis a hash seed, not a
credential, and stays visible).pfsense_get_firewall_nat_one_to_one_mappings— 1:1 NAT mappings
(external/source/destinationredacted the same way).- Owner-authorized, narrowly-scoped live production READ
verification (not LAB) of exactly these two endpoints: both typed
GETs succeeded with zero configured mappings in either category;
field-level type/nullability compatibility confirmed via an exact,
byte-for-byte match between the live OpenAPI schema and the pinned
v2.10 reference the models were derived from.
- 7 new READ tools, public MCP contract 44 → 51 (0 WRITE, unchanged):
pfsense_get_interface_vlans— 802.1Q VLAN interfaces.pfsense_get_routing_static_routes— static routes (network/gateway
redacted by default).pfsense_get_interface_groups— interface group membership, useful
for interpreting firewall rules that target a group.pfsense_get_firewall_schedules— time-based firewall schedules.pfsense_get_system_restapi_version— installed REST API package
version and update availability.pfsense_get_firewall_virtual_ips— CARP/IP-alias/proxy-ARP virtual
IPs (address fields redacted by default; the CARP shared secret is
never modeled at all — see "Security" below).pfsense_get_system_certificate_authorities— trusted CA inventory
(the CA private key is never modeled at all — see "Security" below).- Two of the seven (
system_restapi_version,
system_certificate_authorities) were verified against real,
populated LAB objects, not just an empty envelope — the latter
against the LAB's own internal CA.
- 2 more READ tools (P1 Batch A, partial), public MCP contract 51 →
53 (0 WRITE, unchanged):pfsense_get_status_ipsec_sas— live IPsec SA/tunnel status,
including nested child SAs.pfsense_get_status_ipsec_child_sas— live IPsec child SA status.IPsecSaStatus.child_sasembeds fullIPsecChildSaStatusobjects
(schema-confirmed$ref) and is constructed through that model's
own parser for every nested item, not passed through as a raw dict.status/wireguard/tunnels/status/wireguard/peerswere also
implemented and offline-tested this batch but remained unregistered
at first: this LAB did not havepfSense-pkg-WireGuardinstalled,
so live verification was blocked — see the next entry for how this
was resolved.
- 2 more READ tools (P1 Batch A completion), public MCP contract 53 →
55 (0 WRITE, unchanged): owner explicitly authorized installing
pfSense-pkg-WireGuardon the LAB for non-production READ
verification only. Preflight: reconfirmed LAB identity distinct from
production, identified the LAB as the sole pfSense-named VM in its
Proxmox cluster, and took a fresh rollback snapshot before any
change. Post-install: confirmed pfSense/pfREST healthy, re-ran a
52-tool regression subset with zero regressions, then live-verified
both endpoints (HTTP 200, correct envelope, zero configured tunnels/
peers; raw responses inspected directly for unexpected fields).pfsense_get_status_wireguard_tunnels— live WireGuard tunnel
status, including nested peer status.pfsense_get_status_wireguard_peers— live WireGuard peer status.- The package installation was a one-off authenticated LAB
administrative call, made outside and independent of this
project's ownWriteApiClient/WriteEndpointsallow-list
mechanism, which remains untouched and still empty except
FIREWALL_ALIAS_DESCRIPTION— not a WRITE-capability expansion of
the shipped server.
- 4 more READ tools (P1 Batch B), public MCP contract 55 → 59 (0
WRITE, unchanged):pfsense_get_status_openvpn_servers— live OpenVPN server status,
including nested connection/route status.pfsense_get_status_openvpn_clients— live OpenVPN client status.pfsense_get_status_openvpn_server_connections— flat, all-servers
OpenVPN client connection status.pfsense_get_status_openvpn_server_routes— flat, all-servers
OpenVPN client route status.- Resolved the standing open question of whether the standalone
connection/route endpoints duplicateOpenVpnServerStatus's own
nestedconns/routesfields using the pinned schema's own
Parent modeldeclaration (both standalone endpoints declare
Parent model: OpenVPNServerStatus) — the identical structural
relationship already established as non-redundant between
IPsecSaStatus/IPsecChildSaStatus, so all four were implemented
as genuinely independent capabilities rather than assumed
duplicates. OpenVpnServerStatus.conns/.routesembed full
OpenVpnServerConnectionStatus/OpenVpnServerRouteStatusobjects
(schema-confirmed$ref) and are constructed through those
models' own parsers for every nested item, not passed through as
raw dicts.
- 3 more READ tools (P1 Batch C), public MCP contract 59 → 62 (0
WRITE, unchanged):pfsense_get_dns_forwarder_host_overrides— dnsmasq (DNS Forwarder)
host overrides: addresses, aliases, and descriptions.pfsense_get_dns_resolver_domain_overrides— Unbound (DNS Resolver)
domain overrides: forwarding target address and DNS-over-TLS
settings.pfsense_get_dns_resolver_access_lists— Unbound (DNS Resolver)
access lists: allow/deny action and the network ranges each list
applies to.- All three re-checked against the pinned schema for secrets (none
found) and modeled following the existing shipped
DnsResolverHostOverrideprecedent: full field visibility, no
include_identifying_metadataredaction, since address/network
data is the core content of this capability class (the same
rationale already documented forDhcpServer). - All three LAB-verified live:
HTTP 200, zero configured objects
(ENDPOINT_VERIFIED); no package required (base
pfSense/dnsmasq/Unbound features).
- 3 more READ tools (P1 Batch D, interface extras), public MCP
contract 62 → 65 (0 WRITE, unchanged):pfsense_get_interface_available_interfaces— all interfaces
available for assignment (not just already-assigned ones):
identifier, in-use status, hardware boot message.pfsense_get_interface_gres— GRE tunnel interfaces.pfsense_get_interface_laggs— LAGG (link aggregation) interfaces.- All three re-checked against the pinned schema for secrets (none
found).mac(AvailableInterface) and 7 ofInterfaceGRE's 11
fields (tunnel-endpoint addresses) are redacted by default,
matchingInterfaceStatus.macaddrandRoutingStaticRoute's
established conventions;InterfaceLAGG'smembers/laggifstay
visible, matchingInterfaceBridge's established no-redaction
precedent.InterfaceLAGG's proto-conditional fields
(lacptimeout/lagghash/failovermaster) use.get()with the
schema's own declared default, matching theinstall_version
precedent for a field that can be legitimately absent rather than
merely null. interface/available_interfacesLAB-verified with
FIELD_MODEL_LIVE_VERIFIED: 2 real populated objects (the LAB's
actualvtnet0/vtnet1WAN/LAN interfaces), with redaction
confirmed against real data.interface/gresand
interface/laggsboth LAB-verifiedENDPOINT_VERIFIED(zero
configured objects); no package required for any of the three
(base pfSense features).- The models/client methods/
Endpointsentries were implemented and
offline-tested one commit before registration, deliberately
unregistered in the interim, matching the established
"implemented, offline-tested, blocked" precedent from P1 Batch A's
WireGuard pair.
- 5 more READ tools (P1 Batch E, routing + DHCP extras), public MCP
contract 65 → 70 (0 WRITE, unchanged):pfsense_get_routing_gateway_groups— gateway groups: name,
failover trigger, description, prioritized member gateways.pfsense_get_routing_gateway_default— current default IPv4/IPv6
gateway assignment.pfsense_get_dhcp_relay— DHCP Relay configuration.pfsense_get_dhcp_server_address_pools— additional DHCP scopes
across all configured DHCP servers.pfsense_get_dhcp_server_custom_options— DHCP custom options
across all configured DHCP servers.RoutingGatewayGroupPriority.gateway/.virtual_ipand
DefaultGateway.defaultgw4/.defaultgw6(gateway name references)
...