Acceptance — v0.4.0¶
Status: tagged and released on GitHub, but never published to PyPI.
The v0.4.0 git tag and GitHub Release point at this commit and remain
a permanent, unmodified historical record. The PyPI publish attempt for
this release failed (twine check --strict rejected the CI build's
Metadata-Version: 2.5, a Hatchling/twine incompatibility) and was not
retried under this version number, per this project's immutable-release
policy (docs/PYPI_RELEASE.md). The fix and the corrective release are
v0.4.1 — see docs/ACCEPTANCE_v0.4.1.md. Everything else recorded
below — the live-evidence chain, verified=True, the scoped credential,
and every other functional/security claim — is accurate and unaffected;
only this release's PyPI-publication status was corrected after the
fact. The final "Acceptance boundary" section below states precisely
what is, and is not, authorized by this document.
Release scope¶
v0.4.0's public MCP contract is unchanged from v0.3.1: 42 READ tools, 0 WRITE tools under the default profile. No new tool, no new capability, no schema change, no removal.
The headline change is that the one WRITE capability this project has
ever accepted, set_firewall_alias_description_v1
(WriteEndpoints.FIREWALL_ALIAS_DESCRIPTION), is now verified=True.
That flag had been deliberately False since it was introduced —
described in its own module docstring as "an intentional, additional
layer of protection beyond the activation gate, not an oversight" —
pending independent live evidence. That evidence now exists:
- Two real pfSense mutations, both against a disposable, isolated LAB
appliance (
https://pfsense-test.lab.invalid), never production or home pfSense, each driven through the complete, unmodified production Tier 1 ceremony (fresh authorization → off-host signature → one-time consumption →RecoveryContract→ confirmation → off-host signature → sealed executor). Both reachedRecoveryContractstateVERIFIEDwith a clean, MAC-authenticated audit trail and zero rollback/reconciliation events. The TPM anti-rollback witness advanced exactly once per mutation (2 → 3, then3 → 4), independently confirmed against the persisted high-water mark both times. - The second of those two mutations was performed entirely through a
dedicated, least-privilege pfSense identity (
pfsense_mcp_tier1_lab) holding exactly the four privileges the production path needs — never the administrative account — with its privilege grant independently re-read and confirmed unchanged both before and after the mutation. - A strict, owner-confirmed re-check of every ADR-026 acceptance-matrix row, evaluated individually against its own exact wording (not grandfathered from any earlier pass), confirmed every row satisfied — either by this live evidence directly, or by production-bound offline tests exercising the real Tier1 composition with only the external pfSense transport substituted.
See docs/adr/ADR-026-first-write-capability-adapter.md for the complete
evidence chain, and docs/SECURITY_MODEL.md's "Recovery and WRITE
status" section for the current, precise description of what
verified=True does and does not expose.
verified=True does not enable WRITE by default. Reaching the tool
still requires every one of these, simultaneously:
- An operator explicitly selecting
PFSENSE_PROFILE=write_protected(never the default). - The endpoint's allow-list entry (present, but inert without the other two conditions).
- A successfully constructed production runtime — itself requiring the
full Tier 1 security material (pinned Ed25519 authorities, a
provisioned encrypted
RecoveryContractstore, live TPM witness connectivity) to be independently configured; a deployment missing any of it getsNoneback frombuild_production_runtime()and the tool is never registered, regardless of profile orverified.
And even once reachable, every individual mutation still requires the operator to personally drive the full signing ceremony described above — nothing about it is automatic, and no MCP client, AI session, or automated process can complete it unattended.
Accepted changes¶
src/pfsense_mcp/write_endpoints.py:FIREWALL_ALIAS_DESCRIPTION.verifiedFalse → True, with the module docstring rewritten to state the exact live-evidence chain that justifies it.- Two "tripwire" tests specifically designed to fire the moment this flag
changed (
tests/tier1/test_acceptance.py,tests/tier1/test_acceptance_isolation.py) converted into positive assertions of the new state — including a new regression provingissue_acceptance_context()now permanently refuses the real endpoint (its one-time acceptance-evidence-gathering path is retired for this endpoint, by design, onceverified=Trueholds). docs/adr/ADR-026-first-write-capability-adapter.md: full live-evidence sections recording both mutations, the least-privilege credential proof, and the strict row-by-row re-check.- One new offline regression test closing a genuine gap found during the
strict re-check (
test_production_adapter_send_timeout_reaches_ reconciliation_not_failed_or_resend) — aTransportTimeoutErroron the real adapter's send correctly lands inRECONCILIATION, never licenses a resend. RecoveryContract.is_permanently_unresumable(): a pure, additive, read-only observability helper for the (harmless, historical, never deleted) deadPREPAREDcontracts left by earlier ceremony attempts that expired before completion.PfSenseUser.expires: nullable-model correctness fix (the live API legitimately returnsnull, not"", when no expiration is set) found while gathering least-privilege evidence.PfSenseClient.get_config_history_revisions(): new, narrow READ capability added to gather config-history side-effect evidence (ADR-026 row 18); not part of the production WRITE path.- Documentation consistency pass across
docs/SECURITY_MODEL.md,docs/THREAT_MODEL.md,README.md,SECURITY.md,docs/tier1/IMPLEMENTATION_ROADMAP.md, and everyexamples/*.mdMCP client guide, correcting stale "WRITE is categorically inert/absent" claims and stale41-tool counts (the 42nd tool,pfsense_mcp_info, shipped in v0.3.1) to the current, accurate state. examples/README.mdgained a new section explicitly documenting thewrite_protectedprofile opt-in, since no client guide previously mentioned it at all.
CI evidence¶
To be finalized against the exact release-preparation commit before
tagging — this section must record that commit's own successful CI
(pytest across supported Python versions, coverage, docs, Bandit) and
CodeQL run URLs, matching the discipline every prior ACCEPTANCE_vX.Y.Z.md
in this repository follows. Not yet filled in as of this document's
initial commit; see the corresponding commit message and git log for
the exact SHA and gh run list for its CI/CodeQL results in the
meantime.
Package verification¶
- Version metadata:
0.4.0; Python 3.11+; supported production platform Linux; MITLicense-Expression; Markdown README; typed-package marker. - Hatchling builds one wheel and one sdist with the expected console entry point.
- Distribution verification requires the license, README, PyPI
procedure, this acceptance document, package source, metadata, and
entry point (
scripts/verify_distribution.pyupdated to requiredocs/ACCEPTANCE_v0.4.0.mdspecifically). - Clean wheel installation/import and configuration-absent fail-closed
startup pass through
make package-check. - Strict Twine checks pass for both artifact types.
- Artifacts exclude credentials, key/private-key files,
.env, AI reports (reports-ai/), local configuration, caches, fixtures, and temporary state — confirmed by direct member-listing inspection of both the wheel and sdist, not only by the automated check.
Security invariants¶
- Credential values and prohibited credential fields remain absent from MCP schemas, outputs, logs, errors, fixtures, documentation, and distributions.
- Production READ transport remains GET-only and independently endpoint-gated.
- Capability profiles remain authoritative; annotations and tool restrictions cannot grant access.
- The Auditor profile registers exactly 42 READ tools without restriction; Engineer registers zero. Both independently re-confirmed this release, unchanged from v0.3.1.
- The WRITE endpoint allow-list contains exactly one entry
(
FIREWALL_ALIAS_DESCRIPTION), which is nowverified=True, but zero WRITE tools register under the default profile — independently confirmed viamake quick's write-capability-inactivity check (0 of 3 *_WRITE capabilities are default-reachable) andmake validate's public MCP contract snapshot (42 tools, unchanged). - Reaching the one gated WRITE tool requires explicit
write_protectedprofile selection and a successfully constructed production runtime and a real, owner-driven signing ceremony per mutation — all three independently re-verified live, twice, this release. - Public CI uses no production configuration, credential, or live pfSense call.
Verification evidence¶
- Full offline pytest (2823 passed, 42 live-skipped), Ruff, mypy,
mkdocs --strict,make quick(11 stages),make validate(20 stages), andmake package-checkpass on the release-state tree. - Bandit, fixture safety, repository security, GET-only, WRITE import-absence, write allow-list scope, WRITE-capability inactivity, and git-identity-leak checks pass.
- Two independently-verified live pfSense mutations against the LAB
appliance (see "Release scope" above) —
docs/adr/ ADR-026-first-write-capability-adapter.mdrecords the complete, independently-reconstructed evidence chain for both, not copied from any intermediate session report. - No production or home pfSense call, credential access, or mutation was performed while preparing this release state.
Compatibility¶
All 42 tool names, inputs, outputs, schemas, and semantics from v0.3.1
are unchanged. This is a non-breaking release for every existing
deployment: the public contract snapshot
(tests/contracts/mcp_public_contract_v0.4.0.json, renamed from the
v0.3.1-era file, content byte-identical) continues to gate any future
drift via make validate.
Known limitations¶
Unchanged from v0.3.1 unless noted: the supported trust boundary remains
a local stdio MCP process controlled by a trusted launcher; Linux remains
the supported production platform for descriptor-bound credential
loading; CodeQL SARIF upload remains an open, undecided option;
GitHub Pages redeployment remains a manual step; dependabot.yml's
cadence remains a deferred judgment call.
New to this release:
- A second WRITE capability, or broadening the one accepted capability's scope, remains explicitly out of scope — this project's own standing roadmap ceiling requires a new, separate, explicit owner decision before either.
- The scoped least-privilege pfSense identity used for the second live
mutation is not (yet) the persistent runtime default in
tier1-lab.env— that file still points at the administrative credential for day-to-day LAB work; switching the default is a separate, low-risk, not-yet-decided follow-up, distinct from this release's own acceptance criteria. - Ceremony TTL/operator-UX improvements, the
pfsense-mcp-security setupwizard, and a full GitHub Actions/release-workflow audit were reviewed to varying depth as part of this release's preparation — see the corresponding sections of the preparation record for exactly what was and was not completed.
Acceptance boundary¶
This document accepts the v0.4.0 release-candidate state at its
preparation commit, once that commit passes the required local and
remote gates (CI, CodeQL, make release-check). It does not
authorize a tag, push of a tag, GitHub Release, TestPyPI/PyPI upload,
live pfSense access beyond what is already recorded above, credential
rotation, or any further WRITE. Each of those remains a separate,
explicit, owner-only action.